Pecan: Get loaded by paste deploy

This sets up the factory methods needed to have paste deploy create the
pecan app and return it.  It also changes the legacy factory methods to
conditionally use the pecan factory methods if the web_framework config
option is set to 'pecan'.  This way, all deployments of neutron will not
need to change their api-paste.ini files to get pecan toggled on.  It
should just happen without notice once pecan becomes the default.

Also, by moving this to be loaded by paste deploy, there is a good chunk of
code that has been removed because it is no longer necessary.

Co-Authored-By: Brandon Logan <brandon.logan@rackspace.com>
Change-Id: I8b1bbea8d90fdc62715cd8b6738ad955df53d7cd
This commit is contained in:
tonytan4ever 2016-09-01 23:40:49 -04:00 committed by Brandon Logan
parent e6fd02d941
commit ebc7e1fb2f
15 changed files with 209 additions and 189 deletions

View File

@ -28,6 +28,7 @@ from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron import manager
from neutron.pecan_wsgi import app as pecan_app
from neutron import policy
from neutron.quota import resource_registry
from neutron import wsgi
@ -70,6 +71,8 @@ class APIRouter(base_wsgi.Router):
@classmethod
def factory(cls, global_config, **local_config):
if cfg.CONF.web_framework == 'pecan':
return pecan_app.v2_factory(global_config, **local_config)
return cls(**local_config)
def __init__(self, **local_config):

View File

@ -13,11 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
import oslo_i18n
import webob.dec
from neutron._i18n import _
from neutron.api.views import versions as versions_view
from neutron.pecan_wsgi import app as pecan_app
from neutron import wsgi
@ -25,6 +27,8 @@ class Versions(object):
@classmethod
def factory(cls, global_config, **local_config):
if cfg.CONF.web_framework == 'pecan':
return pecan_app.versions_factory(global_config, **local_config)
return cls(app=None)
@webob.dec.wsgify(RequestClass=wsgi.Request)

View File

@ -10,23 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from neutron import server
from neutron.server import rpc_eventlet
from neutron.server import wsgi_eventlet
from neutron.server import wsgi_pecan
def main():
server.boot_server(_main_neutron_server)
def _main_neutron_server():
if cfg.CONF.web_framework == 'legacy':
wsgi_eventlet.eventlet_wsgi_server()
else:
wsgi_pecan.pecan_wsgi_server()
server.boot_server(wsgi_eventlet.eventlet_wsgi_server)
def main_rpc_eventlet():

View File

@ -13,37 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystonemiddleware import auth_token
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_middleware import cors
from oslo_middleware import http_proxy_to_wsgi
from oslo_middleware import request_id
import pecan
from neutron.api import versions
from neutron.pecan_wsgi.controllers import root
from neutron.pecan_wsgi import hooks
from neutron.pecan_wsgi import startup
CONF = cfg.CONF
CONF.import_opt('bind_host', 'neutron.conf.common')
CONF.import_opt('bind_port', 'neutron.conf.common')
def versions_factory(global_config, **local_config):
return pecan.make_app(root.RootController())
def setup_app(*args, **kwargs):
config = {
'server': {
'port': CONF.bind_port,
'host': CONF.bind_host
},
'app': {
'root': 'neutron.pecan_wsgi.controllers.root.RootController',
'modules': ['neutron.pecan_wsgi'],
}
#TODO(kevinbenton): error templates
}
pecan_config = pecan.configuration.conf_from_dict(config)
def v2_factory(global_config, **local_config):
app_hooks = [
hooks.ExceptionTranslationHook(), # priority 100
hooks.ContextHook(), # priority 95
@ -54,51 +35,10 @@ def setup_app(*args, **kwargs):
hooks.QueryParametersHook(), # priority 139
hooks.PolicyHook(), # priority 140
]
app = pecan.make_app(
pecan_config.app.root,
debug=False,
wrap_app=_wrap_app,
force_canonical=False,
hooks=app_hooks,
guess_content_type_from_ext=True
)
app = pecan.make_app(root.V2Controller(),
debug=False,
force_canonical=False,
hooks=app_hooks,
guess_content_type_from_ext=True)
startup.initialize_all()
return app
def _wrap_app(app):
app = request_id.RequestId(app)
if cfg.CONF.auth_strategy == 'noauth':
pass
elif cfg.CONF.auth_strategy == 'keystone':
app = auth_token.AuthProtocol(app, {})
else:
raise n_exc.InvalidConfigurationOption(
opt_name='auth_strategy', opt_value=cfg.CONF.auth_strategy)
# version can be unauthenticated so it goes outside of auth
app = versions.Versions(app)
# handle cases where neutron-server is behind a proxy
app = http_proxy_to_wsgi.HTTPProxyToWSGI(app)
# This should be the last middleware in the list (which results in
# it being the first in the middleware chain). This is to ensure
# that any errors thrown by other middleware, such as an auth
# middleware - are annotated with CORS headers, and thus accessible
# by the browser.
app = cors.CORS(app, cfg.CONF)
cors.set_defaults(
allow_headers=['X-Auth-Token', 'X-Identity-Status', 'X-Roles',
'X-Service-Catalog', 'X-User-Id', 'X-Tenant-Id',
'X-OpenStack-Request-ID',
'X-Trace-Info', 'X-Trace-HMAC'],
allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
expose_headers=['X-Auth-Token', 'X-Subject-Token', 'X-Service-Token',
'X-OpenStack-Request-ID',
'X-Trace-Info', 'X-Trace-HMAC']
)
return app

View File

@ -38,7 +38,9 @@ class ExtensionsController(object):
@utils.when(index, method='HEAD')
@utils.when(index, method='PATCH')
def not_supported(self):
pecan.abort(405)
# NOTE(blogan): Normally we'd return 405 but the legacy extensions
# controller returned 404.
pecan.abort(404)
class ExtensionController(object):
@ -62,4 +64,6 @@ class ExtensionController(object):
@utils.when(index, method='HEAD')
@utils.when(index, method='PATCH')
def not_supported(self):
pecan.abort(405)
# NOTE(blogan): Normally we'd return 405 but the legacy extensions
# controller returned 404.
pecan.abort(404)

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_log import log
import pecan
from pecan import request
@ -24,6 +25,9 @@ from neutron import manager
from neutron.pecan_wsgi.controllers import extensions as ext_ctrl
from neutron.pecan_wsgi.controllers import utils
CONF = cfg.CONF
LOG = log.getLogger(__name__)
_VERSION_INFO = {}
@ -41,12 +45,16 @@ class RootController(object):
@utils.expose(generic=True)
def index(self):
# NOTE(kevinbenton): The pecan framework does not handle
# any requests to the root because they are intercepted
# by the 'version' returning wrapper.
pass
version_objs = [
{
"id": "v2.0",
"status": "CURRENT",
},
]
builder = versions_view.get_view_builder(pecan.request)
versions = [builder.build(version) for version in version_objs]
return dict(versions=versions)
@utils.when(index, method='GET')
@utils.when(index, method='HEAD')
@utils.when(index, method='POST')
@utils.when(index, method='PATCH')
@ -66,6 +74,11 @@ class V2Controller(object):
}
_load_version_info(version_info)
# NOTE(blogan): Paste deploy handled the routing to the legacy extension
# controller. If the extensions filter is removed from the api-paste.ini
# then this controller will be routed to This means operators had
# the ability to turn off the extensions controller via tha api-paste but
# will not be able to turn it off with the pecan switch.
extensions = ext_ctrl.ExtensionsController()
@utils.expose(generic=True)
@ -112,8 +125,3 @@ class V2Controller(object):
# with the uri_identifiers
request.context['uri_identifiers'] = {}
return controller, remainder
# This controller cannot be specified directly as a member of RootController
# as its path is not a valid python identifier
pecan.route(RootController, 'v2.0', V2Controller())

View File

@ -13,41 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_middleware import request_id
from pecan import hooks
from neutron import context
class ContextHook(hooks.PecanHook):
"""Configures a request context and attaches it to the request.
The following HTTP request headers are used:
X-User-Id or X-User:
Used for context.user_id.
X-Project-Id:
Used for context.tenant_id.
X-Project-Name:
Used for context.tenant_name.
X-Auth-Token:
Used for context.auth_token.
X-Roles:
Used for setting context.is_admin flag to either True or False.
The flag is set to True, if X-Roles contains either an administrator
or admin substring. Otherwise it is set to False.
"""
"""Moves the request env's neutron.context into the requests context."""
priority = 95
def before(self, state):
user_name = state.request.headers.get('X-User-Name', '')
tenant_name = state.request.headers.get('X-Project-Name')
req_id = state.request.headers.get(request_id.ENV_REQUEST_ID)
# TODO(kevinbenton): is_admin logic
# Create a context with the authentication data
ctx = context.Context.from_environ(state.request.environ,
user_name=user_name,
tenant_name=tenant_name,
request_id=req_id)
# Inject the context...
ctx = state.request.environ['neutron.context']
state.request.context['neutron_context'] = ctx

View File

@ -183,11 +183,14 @@ class PolicyHook(hooks.PecanHook):
policy_method(neutron_context, action, item,
plugin=plugin,
pluralized=collection))]
except oslo_policy.PolicyNotAuthorized as e:
except oslo_policy.PolicyNotAuthorized:
# This exception must be explicitly caught as the exception
# translation hook won't be called if an error occurs in the
# 'after' handler.
raise webob.exc.HTTPForbidden(str(e))
# 'after' handler. Instead of raising an HTTPForbidden exception,
# we have to set the status_code here to prevent the catch_errors
# middleware from turning this into a 500.
state.response.status_code = 403
return
if is_single:
resp = resp[0]

View File

@ -18,7 +18,6 @@ from neutron_lib.plugins import directory
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron.api.v2 import router
from neutron import manager
from neutron.pecan_wsgi.controllers import resource as res_ctrl
from neutron.pecan_wsgi.controllers import utils
@ -26,6 +25,16 @@ from neutron import policy
from neutron.quota import resource_registry
# NOTE(blogan): This currently already exists in neutron.api.v2.router but
# instead of importing that module and creating circular imports elsewhere,
# it's easier to just copy it here. The likelihood of it needing to be changed
# are slim to none.
RESOURCES = {'network': 'networks',
'subnet': 'subnets',
'subnetpool': 'subnetpools',
'port': 'ports'}
def initialize_all():
manager.init()
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
@ -33,7 +42,7 @@ def initialize_all():
# At this stage we have a fully populated resource attribute map;
# build Pecan controllers and routes for all core resources
plugin = directory.get_plugin()
for resource, collection in router.RESOURCES.items():
for resource, collection in RESOURCES.items():
resource_registry.register_resource_by_name(resource)
new_controller = res_ctrl.CollectionsController(collection, resource,
plugin=plugin)

View File

@ -1,28 +0,0 @@
#!/usr/bin/env python
# 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.
from oslo_log import log
from neutron._i18n import _LI
from neutron.pecan_wsgi import app as pecan_app
from neutron.server import wsgi_eventlet
from neutron import service
LOG = log.getLogger(__name__)
def pecan_wsgi_server():
LOG.info(_LI("Pecan WSGI server starting..."))
application = pecan_app.setup_app()
neutron_api = service.run_wsgi_app(application)
wsgi_eventlet.start_api_and_rpc_workers(neutron_api)

View File

@ -0,0 +1,45 @@
[composite:neutron]
use = egg:Paste#urlmap
/: neutronversions_composite
/v2.0: neutronapi_v2_0
[composite:neutronapi_v2_0]
use = call:neutron.auth:pipeline_factory
noauth = cors http_proxy_to_wsgi request_id catch_errors extensions neutronapiapp_v2_0
keystone = cors http_proxy_to_wsgi request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
[composite:neutronversions_composite]
use = call:neutron.auth:pipeline_factory
noauth = cors http_proxy_to_wsgi neutronversions
keystone = cors http_proxy_to_wsgi neutronversions
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:catch_errors]
paste.filter_factory = oslo_middleware:CatchErrors.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = neutron
[filter:http_proxy_to_wsgi]
paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory
[filter:keystonecontext]
paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:extensions]
paste.filter_factory = neutron.api.extensions:plugin_aware_extension_middleware_factory
[app:neutronversions]
paste.app_factory = neutron.api.versions:Versions.factory
[app:neutronapiapp_v2_0]
paste.app_factory = neutron.api.v2.router:APIRouter.factory
[filter:osprofiler]
paste.filter_factory = osprofiler.web:WsgiMiddleware.factory

View File

@ -81,11 +81,11 @@ class TestRootController(test_functional.PecanFunctionalTest):
self.assertEqual(value, versions[0][attr])
def test_methods(self):
self._test_method_returns_code('post')
self._test_method_returns_code('patch')
self._test_method_returns_code('delete')
self._test_method_returns_code('head')
self._test_method_returns_code('put')
self._test_method_returns_code('post', 405)
self._test_method_returns_code('patch', 405)
self._test_method_returns_code('delete', 405)
self._test_method_returns_code('head', 405)
self._test_method_returns_code('put', 405)
class TestV2Controller(TestRootController):
@ -146,12 +146,12 @@ class TestExtensionsController(TestRootController):
self.assertEqual(test_alias, json_body['extension']['alias'])
def test_methods(self):
self._test_method_returns_code('post', 405)
self._test_method_returns_code('put', 405)
self._test_method_returns_code('patch', 405)
self._test_method_returns_code('delete', 405)
self._test_method_returns_code('head', 405)
self._test_method_returns_code('delete', 405)
self._test_method_returns_code('post', 404)
self._test_method_returns_code('put', 404)
self._test_method_returns_code('patch', 404)
self._test_method_returns_code('delete', 404)
self._test_method_returns_code('head', 404)
self._test_method_returns_code('delete', 404)
class TestQuotasController(test_functional.PecanFunctionalTest):

View File

@ -19,23 +19,60 @@ import mock
from neutron_lib import constants
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_middleware import base
from oslo_service import wsgi
from oslo_utils import uuidutils
from pecan import set_config
from pecan.testing import load_test_app
import testtools
import webob.dec
import webtest
from neutron.api import extensions as exts
from neutron import context
from neutron import manager
from neutron import tests
from neutron.tests.unit import testlib_api
class InjectContext(base.ConfigurableMiddleware):
@webob.dec.wsgify
def __call__(self, req):
user_id = req.headers.get('X_USER_ID', '')
# Determine the tenant
tenant_id = req.headers.get('X_PROJECT_ID')
# Suck out the roles
roles = [r.strip() for r in req.headers.get('X_ROLES', '').split(',')]
# Human-friendly names
tenant_name = req.headers.get('X_PROJECT_NAME')
user_name = req.headers.get('X_USER_NAME')
# Create a context with the authentication data
ctx = context.Context(user_id, tenant_id, roles=roles,
user_name=user_name, tenant_name=tenant_name)
req.environ['neutron.context'] = ctx
return self.application
def create_test_app():
paste_config_loc = os.path.join(os.path.dirname(tests.__file__), 'etc',
'api-paste.ini')
paste_config_loc = os.path.abspath(paste_config_loc)
cfg.CONF.set_override('api_paste_config', paste_config_loc)
loader = wsgi.Loader(cfg.CONF)
app = loader.load_app('neutron')
app = InjectContext(app)
return webtest.TestApp(app)
class PecanFunctionalTest(testlib_api.SqlTestCase):
def setUp(self, service_plugins=None, extensions=None):
self.setup_coreplugin('ml2', load_plugins=False)
super(PecanFunctionalTest, self).setUp()
self.addCleanup(exts.PluginAwareExtensionManager.clear_instance)
self.addCleanup(set_config, {}, overwrite=True)
self.set_config_overrides()
manager.init()
ext_mgr = exts.PluginAwareExtensionManager.get_instance()
@ -45,15 +82,10 @@ class PecanFunctionalTest(testlib_api.SqlTestCase):
service_plugins[constants.CORE] = ext_mgr.plugins.get(
constants.CORE)
ext_mgr.plugins = service_plugins
self.setup_app()
def setup_app(self):
self.app = load_test_app(os.path.join(
os.path.dirname(__file__),
'config.py'
))
self.app = create_test_app()
def set_config_overrides(self):
cfg.CONF.set_override('web_framework', 'pecan')
cfg.CONF.set_override('auth_strategy', 'noauth')
def do_request(self, url, tenant_id=None, admin=False,
@ -109,8 +141,12 @@ class TestInvalidAuth(PecanFunctionalTest):
def test_invalid_auth_strategy(self):
cfg.CONF.set_override('auth_strategy', 'badvalue')
with testtools.ExpectedException(n_exc.InvalidConfigurationOption):
load_test_app(os.path.join(os.path.dirname(__file__), 'config.py'))
# NOTE(blogan): the auth.pipeline_factory will throw a KeyError
# with a bad value because that value is not the paste config.
# This KeyError is translated to a LookupError, which the oslo wsgi
# code translates into PasteAppNotFound.
with testtools.ExpectedException(wsgi.PasteAppNotFound):
create_test_app()
class TestExceptionTranslationHook(PecanFunctionalTest):

View File

@ -0,0 +1,34 @@
# 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.
import mock
from oslo_config import cfg
from neutron.api import versions
from neutron.tests import base
@mock.patch('neutron.api.versions.Versions.__init__', return_value=None)
@mock.patch('neutron.pecan_wsgi.app.versions_factory')
class TestVersions(base.BaseTestCase):
def test_legacy_factory(self, pecan_mock, legacy_mock):
cfg.CONF.set_override('web_framework', 'legacy')
versions.Versions.factory({})
pecan_mock.assert_not_called()
legacy_mock.assert_called_once_with(app=None)
def test_pecan_factory(self, pecan_mock, legacy_mock):
cfg.CONF.set_override('web_framework', 'pecan')
versions.Versions.factory({})
pecan_mock.assert_called_once_with({})
legacy_mock.assert_not_called()

View File

@ -13,22 +13,22 @@
import mock
from oslo_config import cfg
from neutron.cmd.eventlet import server
from neutron.api.v2 import router
from neutron.tests import base
@mock.patch('neutron.server.wsgi_eventlet.eventlet_wsgi_server')
@mock.patch('neutron.server.wsgi_pecan.pecan_wsgi_server')
class TestNeutronServer(base.BaseTestCase):
@mock.patch('neutron.api.v2.router.APIRouter.__init__', return_value=None)
@mock.patch('neutron.pecan_wsgi.app.v2_factory')
class TestRouter(base.BaseTestCase):
def test_legacy_server(self, pecan_mock, legacy_mock):
def test_legacy_factory(self, pecan_mock, legacy_mock):
cfg.CONF.set_override('web_framework', 'legacy')
server._main_neutron_server()
router.APIRouter.factory({})
pecan_mock.assert_not_called()
legacy_mock.assert_called_with()
legacy_mock.assert_called_once_with()
def test_pecan_server(self, pecan_mock, legacy_mock):
def test_pecan_factory(self, pecan_mock, legacy_mock):
cfg.CONF.set_override('web_framework', 'pecan')
server._main_neutron_server()
pecan_mock.assert_called_with()
router.APIRouter.factory({})
pecan_mock.assert_called_once_with({})
legacy_mock.assert_not_called()