From: Kevin Benton Date: Thu, 11 Jun 2015 08:04:24 +0000 (-0700) Subject: Initial pecan structure X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=dfd4c801d27a158102ebddcf6886f07f9838f88d;p=openstack-build%2Fneutron-build.git Initial pecan structure This patch is the initial work for the pecan refactor. * Adds pecan as a requirement * Adds a simple API server named 'neutron-dev-server' for use when neutron server is not deployed in a web server * Wraps the app with the openstack request ID middleware * Adds a basic V2 controller that breaks out requests by method * Adds functional tests to ensure request ID is set and requests are properly sent to the V2 controller. Partially-Implements: blueprint wsgi-pecan-switch Co-Authored-By: Brandon Logan Co-Authored-By: Mark McClain Change-Id: Ic9697ff30ab8359b62ce01eb73dc927065a8e3e6 --- diff --git a/neutron/cmd/eventlet/api.py b/neutron/cmd/eventlet/api.py new file mode 100644 index 000000000..bdedd2690 --- /dev/null +++ b/neutron/cmd/eventlet/api.py @@ -0,0 +1,68 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# Copyright 2014 Yahoo Inc +# 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. + +# Much of this module is based on the work of the Ironic team +# see http://git.openstack.org/cgit/openstack/ironic/tree/ironic/cmd/api.py + +import logging as std_logging +import sys +from wsgiref import simple_server + +from oslo_config import cfg +from oslo_log import log as logging +from six.moves import socketserver + +from neutron.common import config +from neutron.newapi import app +from neutron.i18n import _LI, _LW + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class ThreadedSimpleServer(socketserver.ThreadingMixIn, + simple_server.WSGIServer): + """A Mixin class to make the API service greenthread-able.""" + pass + + +def main(): + config.init(sys.argv[1:]) + config.setup_logging() + application = app.setup_app() + + host = CONF.bind_host + port = CONF.bind_port + + wsgi = simple_server.make_server( + host, + port, + application, + server_class=ThreadedSimpleServer + ) + + LOG.warning( + _LW("Stand-alone Server Serving on http://%(host)s:%(port)s"), + {'host': host, 'port': port} + ) + LOG.info(_LI("Configuration:")) + CONF.log_opt_values(LOG, std_logging.INFO) + + try: + wsgi.serve_forever() + except KeyboardInterrupt: + pass diff --git a/neutron/newapi/__init__.py b/neutron/newapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/newapi/app.py b/neutron/newapi/app.py new file mode 100644 index 000000000..f434c3dea --- /dev/null +++ b/neutron/newapi/app.py @@ -0,0 +1,55 @@ +# Copyright (c) 2015 Mirantis, Inc. +# 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. + +from oslo_config import cfg +from oslo_middleware import request_id +import pecan + +CONF = cfg.CONF +CONF.import_opt('bind_host', 'neutron.common.config') +CONF.import_opt('bind_port', 'neutron.common.config') + + +def setup_app(*args, **kwargs): + config = { + 'server': { + 'port': CONF.bind_port, + 'host': CONF.bind_host + }, + 'app': { + 'root': 'neutron.newapi.controllers.root.RootController', + 'modules': ['neutron.newapi'], + } + #TODO(kevinbenton): error templates + } + pecan_config = pecan.configuration.conf_from_dict(config) + + app_hooks = [] + + 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 + ) + + return app + + +def _wrap_app(app): + app = request_id.RequestId(app) + return app diff --git a/neutron/newapi/controllers/__init__.py b/neutron/newapi/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/newapi/controllers/root.py b/neutron/newapi/controllers/root.py new file mode 100644 index 000000000..f12ec2160 --- /dev/null +++ b/neutron/newapi/controllers/root.py @@ -0,0 +1,79 @@ +# Copyright (c) 2015 Mirantis, Inc. +# Copyright (c) 2015 Rackspace, Inc. +# 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. + +import pecan + + +def expose(*args, **kwargs): + """Helper function so we don't have to specify json for everything.""" + kwargs.setdefault('content_type', 'application/json') + kwargs.setdefault('template', 'json') + return pecan.expose(*args, **kwargs) + + +def when(index, *args, **kwargs): + """Helper function so we don't have to specify json for everything.""" + kwargs.setdefault('content_type', 'application/json') + kwargs.setdefault('template', 'json') + return index.when(*args, **kwargs) + + +class RootController(object): + + @expose() + def _lookup(self, version, *remainder): + if version == 'v2.0': + return V2Controller(), remainder + + @expose(generic=True) + def index(self): + #TODO(kevinbenton): return a version list + return dict(message='A neutron server') + + +class V2Controller(object): + + @expose() + def _lookup(self, endpoint, *remainder): + return GeneralController(endpoint), remainder + + +class GeneralController(object): + + def __init__(self, token): + self.token = token + + @expose() + def _lookup(self, token, *remainder): + return GeneralController(token), remainder + + @expose(generic=True) + def index(self): + if pecan.request.method != 'GET': + pecan.abort(405) + return {'message': 'GET'} + + @when(index, method='PUT') + def put(self, **kw): + return {'message': 'PUT'} + + @when(index, method='POST') + def post(self, **kw): + return {'message': 'POST'} + + @when(index, method='DELETE') + def delete(self): + return {'message': 'DELETE'} diff --git a/neutron/tests/functional/newapi/__init__.py b/neutron/tests/functional/newapi/__init__.py new file mode 100644 index 000000000..045c268ca --- /dev/null +++ b/neutron/tests/functional/newapi/__init__.py @@ -0,0 +1,39 @@ +# Copyright (c) 2015 Mirantis, Inc. +# 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. + + +import os +from pecan import set_config +from pecan.testing import load_test_app +from unittest import TestCase + + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """ + Used for functional tests where you need to test your + literal application and its integration with the framework. + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/neutron/tests/functional/newapi/config.py b/neutron/tests/functional/newapi/config.py new file mode 100644 index 000000000..056df2ade --- /dev/null +++ b/neutron/tests/functional/newapi/config.py @@ -0,0 +1,25 @@ +# Copyright (c) 2015 Mirantis, Inc. +# 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. + +# use main app settings except for the port number so testing doesn't need to +# listen on the main neutron port +app = { + 'root': 'neutron.newapi.controllers.root.RootController', + 'modules': ['neutron.newapi'], + 'errors': { + 400: '/error', + '__force_dict__': True + } +} diff --git a/neutron/tests/functional/newapi/test_functional.py b/neutron/tests/functional/newapi/test_functional.py new file mode 100644 index 000000000..cf87425fa --- /dev/null +++ b/neutron/tests/functional/newapi/test_functional.py @@ -0,0 +1,78 @@ +# Copyright (c) 2015 Mirantis, Inc. +# 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. + +import os + +from oslo_utils import uuidutils +from pecan import set_config +from pecan.testing import load_test_app + +from neutron.tests.unit import testlib_api + + +class PecanFunctionalTest(testlib_api.SqlTestCase): + + def setUp(self): + self.setup_coreplugin('neutron.plugins.ml2.plugin.Ml2Plugin') + super(PecanFunctionalTest, self).setUp() + self.addCleanup(set_config, {}, overwrite=True) + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + +class TestV2Controller(PecanFunctionalTest): + + def test_get(self): + response = self.app.get('/v2.0/ports.json') + self.assertEqual(response.status_int, 200) + + def test_post(self): + response = self.app.post_json('/v2.0/ports.json', + params={'port': {'name': 'test'}}) + self.assertEqual(response.status_int, 200) + + def test_put(self): + response = self.app.put_json('/v2.0/ports/44.json', + params={'port': {'name': 'test'}}) + self.assertEqual(response.status_int, 200) + + def test_delete(self): + response = self.app.delete('/v2.0/ports/44.json') + self.assertEqual(response.status_int, 200) + + +class TestErrors(PecanFunctionalTest): + + def test_404(self): + response = self.app.get('/assert_called_once', expect_errors=True) + self.assertEqual(response.status_int, 404) + + def test_bad_method(self): + response = self.app.patch('/v2.0/', + expect_errors=True) + self.assertEqual(response.status_int, 405) + + +class TestRequestID(PecanFunctionalTest): + + def test_request_id(self): + response = self.app.get('/') + self.assertIn('x-openstack-request-id', response.headers) + self.assertTrue( + response.headers['x-openstack-request-id'].startswith('req-')) + id_part = response.headers['x-openstack-request-id'].split('req-')[1] + self.assertTrue(uuidutils.is_uuid_like(id_part)) diff --git a/requirements.txt b/requirements.txt index 2d7c1ad36..13fc17d03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ Routes!=2.0,!=2.1,>=1.12.3;python_version=='2.7' Routes!=2.0,>=1.12.3;python_version!='2.7' debtcollector>=0.3.0 # Apache-2.0 eventlet>=0.17.4 +pecan>=0.8.0 greenlet>=0.3.2 httplib2>=0.7.5 requests>=2.5.2 diff --git a/setup.cfg b/setup.cfg index e3f0f05bb..d1ea61c34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,6 +88,7 @@ scripts = console_scripts = neutron-db-manage = neutron.db.migration.cli:main neutron-debug = neutron.debug.shell:main + neutron-dev-server = neutron.cmd.eventlet.api:main neutron-dhcp-agent = neutron.cmd.eventlet.agents.dhcp:main neutron-hyperv-agent = neutron.cmd.eventlet.plugins.hyperv_neutron_agent:main neutron-keepalived-state-change = neutron.cmd.keepalived_state_change:main diff --git a/tools/pecan_server.sh b/tools/pecan_server.sh new file mode 100755 index 000000000..ef4709aae --- /dev/null +++ b/tools/pecan_server.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Copyright (c) 2015 Mirantis, Inc. +# 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. + +# A script useful to develop changes to the codebase. It launches the pecan +# API server and will reload it whenever the code changes if inotifywait is +# installed. + +inotifywait --help >/dev/null 2>&1 +if [[ $? -ne 1 ]]; then + USE_INOTIFY=0 +else + USE_INOTIFY=1 +fi + +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../ +source "$DIR/.tox/py27/bin/activate" +COMMAND="python -c 'from neutron.cmd.eventlet import api; api.main()'" + +function cleanup() { + kill $PID + exit 0 +} + +if [[ $USE_INOTIFY -eq 1 ]]; then + trap cleanup INT + while true; do + eval "$COMMAND &" + PID=$! + inotifywait -e modify -r $DIR/neutron/ + kill $PID + done +else + eval $COMMAND +fi