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 <brandon.logan@rackspace.com>
Co-Authored-By: Mark McClain <mark@mcclain.xyz>
Change-Id: Ic9697ff30ab8359b62ce01eb73dc927065a8e3e6
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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'}
--- /dev/null
+# 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)
--- /dev/null
+# 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
+ }
+}
--- /dev/null
+# 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))
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
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
--- /dev/null
+#!/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