Merge "Initial pecan structure" into feature/pecan

This commit is contained in:
Jenkins 2015-08-01 10:29:09 +00:00 committed by Gerrit Code Review
commit b894c74add
11 changed files with 393 additions and 0 deletions

View 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

View File

55
neutron/newapi/app.py Normal file
View 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

View File

View 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'}

View 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)

View 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
}
}

View 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))

View 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

View File

@ -87,6 +87,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
View 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