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 <brandon.logan@rackspace.com> Co-Authored-By: Mark McClain <mark@mcclain.xyz> Change-Id: Ic9697ff30ab8359b62ce01eb73dc927065a8e3e6
This commit is contained in:
parent
14b4a0e1d1
commit
dfd4c801d2
68
neutron/cmd/eventlet/api.py
Normal file
68
neutron/cmd/eventlet/api.py
Normal file
@ -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
|
0
neutron/newapi/__init__.py
Normal file
0
neutron/newapi/__init__.py
Normal file
55
neutron/newapi/app.py
Normal file
55
neutron/newapi/app.py
Normal file
@ -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
|
0
neutron/newapi/controllers/__init__.py
Normal file
0
neutron/newapi/controllers/__init__.py
Normal file
79
neutron/newapi/controllers/root.py
Normal file
79
neutron/newapi/controllers/root.py
Normal file
@ -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'}
|
39
neutron/tests/functional/newapi/__init__.py
Normal file
39
neutron/tests/functional/newapi/__init__.py
Normal file
@ -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)
|
25
neutron/tests/functional/newapi/config.py
Normal file
25
neutron/tests/functional/newapi/config.py
Normal file
@ -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
|
||||
}
|
||||
}
|
78
neutron/tests/functional/newapi/test_functional.py
Normal file
78
neutron/tests/functional/newapi/test_functional.py
Normal file
@ -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))
|
@ -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
|
||||
|
@ -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
|
||||
|
47
tools/pecan_server.sh
Executable file
47
tools/pecan_server.sh
Executable file
@ -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
|
Loading…
x
Reference in New Issue
Block a user