]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Initial pecan structure
authorKevin Benton <blak111@gmail.com>
Thu, 11 Jun 2015 08:04:24 +0000 (01:04 -0700)
committerKevin Benton <blak111@gmail.com>
Sat, 1 Aug 2015 08:19:39 +0000 (01:19 -0700)
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

neutron/cmd/eventlet/api.py [new file with mode: 0644]
neutron/newapi/__init__.py [new file with mode: 0644]
neutron/newapi/app.py [new file with mode: 0644]
neutron/newapi/controllers/__init__.py [new file with mode: 0644]
neutron/newapi/controllers/root.py [new file with mode: 0644]
neutron/tests/functional/newapi/__init__.py [new file with mode: 0644]
neutron/tests/functional/newapi/config.py [new file with mode: 0644]
neutron/tests/functional/newapi/test_functional.py [new file with mode: 0644]
requirements.txt
setup.cfg
tools/pecan_server.sh [new file with mode: 0755]

diff --git a/neutron/cmd/eventlet/api.py b/neutron/cmd/eventlet/api.py
new file mode 100644 (file)
index 0000000..bdedd26
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/neutron/newapi/app.py b/neutron/newapi/app.py
new file mode 100644 (file)
index 0000000..f434c3d
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/neutron/newapi/controllers/root.py b/neutron/newapi/controllers/root.py
new file mode 100644 (file)
index 0000000..f12ec21
--- /dev/null
@@ -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 (file)
index 0000000..045c268
--- /dev/null
@@ -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 (file)
index 0000000..056df2a
--- /dev/null
@@ -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 (file)
index 0000000..cf87425
--- /dev/null
@@ -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))
index 2d7c1ad3638b3037fe5d843360d3df6bd3496187..13fc17d03602817dab7aa1aafafe8a3d39c8d532 100644 (file)
@@ -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
index e3f0f05bb9e1adf47fcd3afae4c6215fc475084a..d1ea61c3495648336747d0eaf0489b4dab6e38ae 100644 (file)
--- 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 (executable)
index 0000000..ef4709a
--- /dev/null
@@ -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