Merge remote-tracking branch 'origin/feature/pecan' into merge-branch

Change-Id: I1c0b6a9837273f6a5a24f537d75c1ff43ff599f8
This commit is contained in:
Kyle Mestery 2015-09-19 14:44:06 +00:00
commit e95904060a
31 changed files with 1727 additions and 43 deletions

View File

@ -207,7 +207,8 @@ class ExtensionController(wsgi.Controller):
def __init__(self, extension_manager):
self.extension_manager = extension_manager
def _translate(self, ext):
@staticmethod
def _translate(ext):
ext_data = {}
ext_data['name'] = ext.get_name()
ext_data['alias'] = ext.get_alias()
@ -600,6 +601,10 @@ class PluginAwareExtensionManager(ExtensionManager):
pass
return aliases
@classmethod
def clear_instance(cls):
cls._instance = None
def check_if_plugin_extensions_loaded(self):
"""Check if an extension supported by a plugin has been loaded."""
plugin_extensions = self.get_supported_extension_aliases()

View File

@ -28,13 +28,19 @@ LOG = logging.getLogger(__name__)
def build_plural_mappings(special_mappings, resource_map):
"""Create plural to singular mapping for all resources.
Allows for special mappings to be provided, like policies -> policy.
Allows for special mappings to be provided, for particular cases..
Otherwise, will strip off the last character for normal mappings, like
routers -> router.
routers -> router, unless the plural name ends with 'ies', in which
case the singular form will end with a 'y' (e.g.: policy/policies)
"""
plural_mappings = {}
for plural in resource_map:
singular = special_mappings.get(plural, plural[:-1])
singular = special_mappings.get(plural)
if not singular:
if plural.endswith('ies'):
singular = "%sy" % plural[:-3]
else:
singular = plural[:-1]
plural_mappings[plural] = singular
return plural_mappings

View File

@ -10,8 +10,20 @@
# License for the specific language governing permissions and limitations
# under the License.
from neutron import server
from neutron.server import rpc_eventlet
from neutron.server import wsgi_eventlet
from neutron.server import wsgi_pecan
def main():
server.main()
def main_wsgi_eventlet():
wsgi_eventlet.main()
# Eventlet patching is not required for Pecan, but some plugins still spawn
# eventlet threads
def main_wsgi_pecan():
wsgi_pecan.main()
def main_rpc_eventlet():
rpc_eventlet.main()

View File

@ -47,6 +47,12 @@ TRANSPORT_ALIASES = {
'neutron.rpc.impl_zmq': 'zmq',
}
# NOTE(salv-orlando): I am afraid this is a global variable. While not ideal,
# they're however widely used throughout the code base. It should be set to
# true if the RPC server is not running in the current process space. This
# will prevent get_connection from creating connections to the AMQP server
RPC_DISABLED = False
def init(conf):
global TRANSPORT, NOTIFIER
@ -201,6 +207,25 @@ class Connection(object):
server.wait()
class VoidConnection(object):
def create_consumer(self, topic, endpoints, fanout=False):
pass
def consume_in_threads(self):
pass
def close(self):
pass
# functions
def create_connection(new=True):
# NOTE(salv-orlando): This is a clever interpreation of the factory design
# patter aimed at preventing plugins from initializing RPC servers upon
# initialization when they are running in the REST over HTTP API server.
# The educated reader will perfectly be able that this a fairly dirty hack
# to avoid having to change the initialization process of every plugin.
if RPC_DISABLED:
return VoidConnection()
return Connection()

View File

@ -129,6 +129,9 @@ class NeutronManager(object):
# the rest of service plugins
self.service_plugins = {constants.CORE: self.plugin}
self._load_service_plugins()
# Used by pecan WSGI
self.resource_plugin_mappings = {}
self.resource_controller_mappings = {}
@staticmethod
def load_class_for_provider(namespace, plugin_provider):
@ -251,3 +254,19 @@ class NeutronManager(object):
def get_unique_service_plugins(cls):
service_plugins = cls.get_instance().service_plugins
return tuple(weakref.proxy(x) for x in set(service_plugins.values()))
@classmethod
def set_plugin_for_resource(cls, resource, plugin):
cls.get_instance().resource_plugin_mappings[resource] = plugin
@classmethod
def get_plugin_for_resource(cls, resource):
return cls.get_instance().resource_plugin_mappings.get(resource)
@classmethod
def set_controller_for_resource(cls, resource, controller):
cls.get_instance().resource_controller_mappings[resource] = controller
@classmethod
def get_controller_for_resource(cls, resource):
return cls.get_instance().resource_controller_mappings.get(resource)

View File

77
neutron/pecan_wsgi/app.py Normal file
View File

@ -0,0 +1,77 @@
# 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 keystonemiddleware import auth_token
from oslo_config import cfg
from oslo_middleware import request_id
import pecan
from neutron.common import exceptions as n_exc
from neutron.pecan_wsgi import hooks
from neutron.pecan_wsgi import startup
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.pecan_wsgi.controllers.root.RootController',
'modules': ['neutron.pecan_wsgi'],
}
#TODO(kevinbenton): error templates
}
pecan_config = pecan.configuration.conf_from_dict(config)
app_hooks = [
hooks.ExceptionTranslationHook(), # priority 100
hooks.ContextHook(), # priority 95
hooks.MemberActionHook(), # piority 95
hooks.AttributePopulationHook(), # priority 120
hooks.OwnershipValidationHook(), # priority 125
hooks.QuotaEnforcementHook(), # priority 130
hooks.PolicyHook(), # priority 135
hooks.NotifierHook(), # priority 140
]
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
)
startup.initialize_all()
return app
def _wrap_app(app):
app = request_id.RequestId(app)
if cfg.CONF.auth_strategy == 'noauth':
pass
elif cfg.CONF.auth_strategy == 'keystone':
app = auth_token.AuthProtocol(app, {})
else:
raise n_exc.InvalidConfigurationOption(
opt_name='auth_strategy', opt_value=cfg.CONF.auth_strategy)
return app

View File

@ -0,0 +1,218 @@
# 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.
from oslo_log import log
import pecan
from pecan import request
from neutron.api import extensions
from neutron.api.views import versions as versions_view
from neutron.i18n import _LW
from neutron import manager
LOG = log.getLogger(__name__)
_VERSION_INFO = {}
def _load_version_info(version_info):
assert version_info['id'] not in _VERSION_INFO
_VERSION_INFO[version_info['id']] = version_info
def _get_version_info():
return _VERSION_INFO.values()
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(generic=True)
def index(self):
builder = versions_view.get_view_builder(pecan.request)
versions = [builder.build(version) for version in _get_version_info()]
return dict(versions=versions)
@when(index, method='POST')
@when(index, method='PUT')
@when(index, method='DELETE')
def not_supported(self):
pecan.abort(405)
class ExtensionsController(object):
@expose()
def _lookup(self, alias, *remainder):
return ExtensionController(alias), remainder
@expose()
def index(self):
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
exts = [extensions.ExtensionController._translate(ext)
for ext in ext_mgr.extensions.values()]
return {'extensions': exts}
class V2Controller(object):
# Same data structure as neutron.api.versions.Versions for API backward
# compatibility
version_info = {
'id': 'v2.0',
'status': 'CURRENT'
}
_load_version_info(version_info)
extensions = ExtensionsController()
@expose(generic=True)
def index(self):
builder = versions_view.get_view_builder(pecan.request)
return dict(version=builder.build(self.version_info))
@when(index, method='POST')
@when(index, method='PUT')
@when(index, method='DELETE')
def not_supported(self):
pecan.abort(405)
@expose()
def _lookup(self, collection, *remainder):
controller = manager.NeutronManager.get_controller_for_resource(
collection)
if not controller:
LOG.warn(_LW("No controller found for: %s - returning response "
"code 404"), collection)
pecan.abort(404)
# Store resource name in pecan request context so that hooks can
# leverage it if necessary
request.context['resource'] = controller.resource
return controller, remainder
# This controller cannot be specified directly as a member of RootController
# as its path is not a valid python identifier
pecan.route(RootController, 'v2.0', V2Controller())
class ExtensionController(object):
def __init__(self, alias):
self.alias = alias
@expose()
def index(self):
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
ext = ext_mgr.extensions.get(self.alias, None)
if not ext:
pecan.abort(
404, detail=_("Extension with alias %s "
"does not exist") % self.alias)
return {'extension': extensions.ExtensionController._translate(ext)}
class NeutronPecanController(object):
def __init__(self, collection, resource):
self.collection = collection
self.resource = resource
self.plugin = manager.NeutronManager.get_plugin_for_resource(
self.resource)
class CollectionsController(NeutronPecanController):
@expose()
def _lookup(self, item, *remainder):
return ItemController(self.resource, item), remainder
@expose(generic=True)
def index(self, *args, **kwargs):
return self.get(*args, **kwargs)
def get(self, *args, **kwargs):
# list request
# TODO(kevinbenton): use user-provided fields in call to plugin
# after making sure policy enforced fields remain
kwargs.pop('fields', None)
_listify = lambda x: x if isinstance(x, list) else [x]
filters = {k: _listify(v) for k, v in kwargs.items()}
# TODO(kevinbenton): convert these using api_common.get_filters
lister = getattr(self.plugin, 'get_%s' % self.collection)
neutron_context = request.context.get('neutron_context')
return {self.collection: lister(neutron_context, filters=filters)}
@when(index, method='POST')
def post(self, *args, **kwargs):
# TODO(kevinbenton): emulated bulk!
pecan.response.status = 201
if request.bulk:
method = 'create_%s_bulk' % self.resource
else:
method = 'create_%s' % self.resource
creator = getattr(self.plugin, method)
key = self.collection if request.bulk else self.resource
neutron_context = request.context.get('neutron_context')
return {key: creator(neutron_context, request.prepared_data)}
class ItemController(NeutronPecanController):
def __init__(self, resource, item):
super(ItemController, self).__init__(None, resource)
self.item = item
@expose(generic=True)
def index(self, *args, **kwargs):
return self.get()
def get(self, *args, **kwargs):
getter = getattr(self.plugin, 'get_%s' % self.resource)
neutron_context = request.context.get('neutron_context')
return {self.resource: getter(neutron_context, self.item)}
@when(index, method='PUT')
def put(self, *args, **kwargs):
neutron_context = request.context.get('neutron_context')
if request.member_action:
member_action_method = getattr(self.plugin,
request.member_action)
return member_action_method(neutron_context, self.item,
request.prepared_data)
# TODO(kevinbenton): bulk?
updater = getattr(self.plugin, 'update_%s' % self.resource)
return updater(neutron_context, self.item, request.prepared_data)
@when(index, method='DELETE')
def delete(self):
# TODO(kevinbenton): setting code could be in a decorator
pecan.response.status = 204
neutron_context = request.context.get('neutron_context')
deleter = getattr(self.plugin, 'delete_%s' % self.resource)
return deleter(neutron_context, self.item)

View File

@ -0,0 +1,33 @@
# 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 neutron.pecan_wsgi.hooks import attribute_population
from neutron.pecan_wsgi.hooks import context
from neutron.pecan_wsgi.hooks import member_action
from neutron.pecan_wsgi.hooks import notifier
from neutron.pecan_wsgi.hooks import ownership_validation
from neutron.pecan_wsgi.hooks import policy_enforcement
from neutron.pecan_wsgi.hooks import quota_enforcement
from neutron.pecan_wsgi.hooks import translation
ExceptionTranslationHook = translation.ExceptionTranslationHook
ContextHook = context.ContextHook
MemberActionHook = member_action.MemberActionHook
AttributePopulationHook = attribute_population.AttributePopulationHook
OwnershipValidationHook = ownership_validation.OwnershipValidationHook
PolicyHook = policy_enforcement.PolicyHook
QuotaEnforcementHook = quota_enforcement.QuotaEnforcementHook
NotifierHook = notifier.NotifierHook

View File

@ -0,0 +1,104 @@
# 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 pecan import hooks
from neutron.api.v2 import attributes
from neutron.api.v2 import base as v2base
from neutron import manager
class AttributePopulationHook(hooks.PecanHook):
priority = 120
def before(self, state):
state.request.prepared_data = {}
state.request.resources = []
if state.request.method not in ('POST', 'PUT'):
return
is_create = state.request.method == 'POST'
resource = state.request.context.get('resource')
neutron_context = state.request.context['neutron_context']
if not resource:
return
if state.request.member_action:
# Neutron currently does not describe request bodies for member
# actions in meh. prepare_request_body should not be called for
# member actions, and the body should be passed as it is. The
# plugin will do the validation (yuck).
state.request.prepared_data = state.request.json
else:
state.request.prepared_data = (
v2base.Controller.prepare_request_body(
neutron_context, state.request.json, is_create,
resource, _attributes_for_resource(resource),
allow_bulk=True))
# TODO(kevinbenton): conditional allow_bulk
state.request.resources = _extract_resources_from_state(state)
# make the original object available:
if not is_create and not state.request.member_action:
obj_id = _pull_id_from_request(state.request, resource)
attrs = _attributes_for_resource(resource)
field_list = [name for (name, value) in attrs.items()
if (value.get('required_by_policy') or
value.get('primary_key') or
'default' not in value)]
plugin = manager.NeutronManager.get_plugin_for_resource(resource)
getter = getattr(plugin, 'get_%s' % resource)
# TODO(kevinbenton): the parent_id logic currently in base.py
obj = getter(neutron_context, obj_id, fields=field_list)
state.request.original_object = obj
def _attributes_for_resource(resource):
if resource not in attributes.PLURALS.values():
return {}
return attributes.RESOURCE_ATTRIBUTE_MAP.get(
_plural(resource), {})
def _pull_id_from_request(request, resource):
# NOTE(kevinbenton): this sucks
# Converting /v2.0/ports/dbbdae29-82f6-49cf-b05e-3365bcc95b7a.json
# into dbbdae29-82f6-49cf-b05e-3365bcc95b7a
resources = _plural(resource)
jsontrail = request.path_info.replace('/v2.0/%s/' % resources, '')
obj_id = jsontrail.replace('.json', '')
return obj_id
def _plural(rtype):
for plural, single in attributes.PLURALS.items():
if rtype == single:
return plural
def _extract_resources_from_state(state):
resource = state.request.context['resource']
if not resource:
return []
data = state.request.prepared_data
# single item
if resource in data:
state.request.bulk = False
return [data[resource]]
# multiple items
if _plural(resource) in data:
state.request.bulk = True
return data[_plural(resource)]
return []

View File

@ -0,0 +1,58 @@
# Copyright 2012 New Dream Network, LLC (DreamHost)
# 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_middleware import request_id
from pecan import hooks
from neutron import context
class ContextHook(hooks.PecanHook):
"""Configures a request context and attaches it to the request.
The following HTTP request headers are used:
X-User-Id or X-User:
Used for context.user_id.
X-Tenant-Id or X-Tenant:
Used for context.tenant.
X-Auth-Token:
Used for context.auth_token.
X-Roles:
Used for setting context.is_admin flag to either True or False.
The flag is set to True, if X-Roles contains either an administrator
or admin substring. Otherwise it is set to False.
"""
priority = 95
def before(self, state):
user_id = state.request.headers.get('X-User-Id')
user_id = state.request.headers.get('X-User', user_id)
user_name = state.request.headers.get('X-User-Name', '')
tenant_id = state.request.headers.get('X-Tenant-Id')
tenant_name = state.request.headers.get('X-Tenant-Name')
auth_token = state.request.headers.get('X-Auth-Token')
roles = state.request.headers.get('X-Roles', '').split(',')
roles = [r.strip() for r in roles]
creds = {'roles': roles}
req_id = state.request.headers.get(request_id.ENV_REQUEST_ID)
# TODO(kevinbenton): is_admin logic
# Create a context with the authentication data
ctx = context.Context(user_id, tenant_id=tenant_id,
roles=creds['roles'],
user_name=user_name, tenant_name=tenant_name,
request_id=req_id, auth_token=auth_token)
# Inject the context...
state.request.context['neutron_context'] = ctx

View File

@ -0,0 +1,68 @@
# 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 pecan import abort
from pecan import hooks
from neutron.api import extensions
from neutron.api.v2 import attributes
class MemberActionHook(hooks.PecanHook):
priority = 95
def before(self, state):
# TODO(salv-orlando): This hook must go. Handling actions like this is
# shameful
resource = state.request.context.get('resource')
if not resource:
return
try:
# Remove the format suffix if any
uri = state.request.path.rsplit('.', 1)[0].split('/')[2:]
if not uri:
# there's nothing to process in the URI
return
except IndexError:
return
collection = None
for (collection, res) in attributes.PLURALS.items():
if res == resource:
break
else:
return
state.request.member_action = self._parse_action(
resource, collection, uri[1:])
def _parse_action(self, resource, collection, remainder):
# NOTE(salv-orlando): This check is revolting and makes me
# puke, but avoids silly failures when dealing with API actions
# such as "add_router_interface".
if len(remainder) > 1:
action = remainder[1]
else:
return
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
resource_exts = ext_mgr.get_resources()
for ext in resource_exts:
if (ext.collection == collection and action in ext.member_actions):
return action
# Action or resource extension not found
if action:
abort(404, detail="Action %(action)s for resource "
"%(resource)s undefined" %
{'action': action,
'resource': resource})

View File

@ -0,0 +1,30 @@
# 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 pecan import hooks
class NotifierHook(hooks.PecanHook):
priority = 140
# TODO(kevinbenton): implement
# dhcp agent notifier
# ceilo notifier
# nova notifier
def before(self, state):
pass
def after(self, state):
pass

View File

@ -0,0 +1,56 @@
# 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 pecan import hooks
import webob
from neutron import manager
class OwnershipValidationHook(hooks.PecanHook):
priority = 125
def before(self, state):
if state.request.method != 'POST':
return
items = state.request.resources
for item in items:
self._validate_network_tenant_ownership(state, item)
def _validate_network_tenant_ownership(self, state, resource_item):
# TODO(salvatore-orlando): consider whether this check can be folded
# in the policy engine
neutron_context = state.request.context.get('neutron_context')
resource = state.request.context.get('resource')
if (neutron_context.is_admin or neutron_context.is_advsvc or
resource not in ('port', 'subnet')):
return
plugin = manager.NeutronManager.get_plugin()
network = plugin.get_network(neutron_context,
resource_item['network_id'])
# do not perform the check on shared networks
if network.get('shared'):
return
network_owner = network['tenant_id']
if network_owner != resource_item['tenant_id']:
msg = _("Tenant %(tenant_id)s not allowed to "
"create %(resource)s on this network")
raise webob.exc.HTTPForbidden(msg % {
"tenant_id": resource_item['tenant_id'],
"resource": resource,
})

View File

@ -0,0 +1,145 @@
# 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 copy
import simplejson
from oslo_log import log
from oslo_policy import policy as oslo_policy
from oslo_utils import excutils
import pecan
from pecan import hooks
import webob
from neutron.common import constants as const
from neutron import manager
from neutron.pecan_wsgi.hooks import attribute_population
from neutron import policy
LOG = log.getLogger(__name__)
class PolicyHook(hooks.PecanHook):
priority = 135
ACTION_MAP = {'POST': 'create', 'PUT': 'update', 'GET': 'get',
'DELETE': 'delete'}
def before(self, state):
if state.request.method not in self.ACTION_MAP:
pecan.abort(405)
neutron_context = state.request.context.get('neutron_context')
resource = state.request.context.get('resource')
is_update = (state.request.method == 'PUT')
items = state.request.resources
policy.init()
action = '%s_%s' % (self.ACTION_MAP[state.request.method], resource)
for item in items:
if is_update:
obj = copy.copy(state.request.original_object)
obj.update(item)
obj[const.ATTRIBUTES_TO_UPDATE] = item.keys()
item = obj
try:
policy.enforce(
neutron_context, action, item,
pluralized=attribute_population._plural(resource))
except oslo_policy.PolicyNotAuthorized:
with excutils.save_and_reraise_exception() as ctxt:
# If a tenant is modifying it's own object, it's safe to
# return a 403. Otherwise, pretend that it doesn't exist
# to avoid giving away information.
if (is_update and
neutron_context.tenant_id != obj['tenant_id']):
ctxt.reraise = False
msg = _('The resource could not be found.')
raise webob.exc.HTTPNotFound(msg)
def after(self, state):
neutron_context = state.request.context.get('neutron_context')
resource = state.request.context.get('resource')
if not resource:
# can't filter a resource we don't recognize
return
# NOTE(kevinbenton): extension listing isn't controlled by policy
if resource == 'extension':
return
try:
data = state.response.json
except simplejson.JSONDecodeError:
return
action = '%s_%s' % (self.ACTION_MAP[state.request.method],
resource)
plural = attribute_population._plural(resource)
if not data or (resource not in data and plural not in data):
return
is_single = resource in data
key = resource if is_single else plural
to_process = [data[resource]] if is_single else data[plural]
# in the single case, we enforce which raises on violation
# in the plural case, we just check so violating items are hidden
policy_method = policy.enforce if is_single else policy.check
plugin = manager.NeutronManager.get_plugin_for_resource(resource)
resp = [self._get_filtered_item(state.request, resource, item)
for item in to_process
if (state.request.method != 'GET' or
policy_method(neutron_context, action, item,
plugin=plugin,
pluralized=plural))]
if is_single:
resp = resp[0]
data[key] = resp
state.response.json = data
def _get_filtered_item(self, request, resource, data):
neutron_context = request.context.get('neutron_context')
to_exclude = self._exclude_attributes_by_policy(
neutron_context, resource, data)
return self._filter_attributes(request, data, to_exclude)
def _filter_attributes(self, request, data, fields_to_strip):
# TODO(kevinbenton): this works but we didn't allow the plugin to
# only fetch the fields we are interested in. consider moving this
# to the call
user_fields = request.params.getall('fields')
return dict(item for item in data.items()
if (item[0] not in fields_to_strip and
(not user_fields or item[0] in user_fields)))
def _exclude_attributes_by_policy(self, context, resource, data):
"""Identifies attributes to exclude according to authZ policies.
Return a list of attribute names which should be stripped from the
response returned to the user because the user is not authorized
to see them.
"""
attributes_to_exclude = []
for attr_name in data.keys():
attr_data = attribute_population._attributes_for_resource(
resource).get(attr_name)
if attr_data and attr_data['is_visible']:
if policy.check(
context,
# NOTE(kevinbenton): this used to reference a
# _plugin_handlers dict, why?
'get_%s:%s' % (resource, attr_name),
data,
might_not_exist=True,
pluralized=attribute_population._plural(resource)):
# this attribute is visible, check next one
continue
# if the code reaches this point then either the policy check
# failed or the attribute was not visible in the first place
attributes_to_exclude.append(attr_name)
return attributes_to_exclude

View File

@ -0,0 +1,58 @@
# 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_log import log as logging
from pecan import hooks
from neutron.common import exceptions
from neutron import manager
from neutron.pecan_wsgi.hooks import attribute_population
from neutron import quota
LOG = logging.getLogger(__name__)
class QuotaEnforcementHook(hooks.PecanHook):
priority = 130
def before(self, state):
# TODO(salv-orlando): This hook must go when adaptin the pecan code to
# use reservations.
if state.request.method != 'POST':
return
resource = state.request.context.get('resource')
plugin = manager.NeutronManager.get_plugin_for_resource(resource)
items = state.request.resources
deltas = {}
for item in items:
tenant_id = item['tenant_id']
try:
neutron_context = state.request.context.get('neutron_context')
count = quota.QUOTAS.count(neutron_context,
resource,
plugin,
attribute_population._plural(
resource),
tenant_id)
delta = deltas.get(tenant_id, 0) + 1
kwargs = {resource: count + delta}
except exceptions.QuotaResourceUnknown as e:
# We don't want to quota this resource
LOG.debug(e)
else:
quota.QUOTAS.limit_check(neutron_context, tenant_id,
**kwargs)

View File

@ -0,0 +1,100 @@
# 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 pecan import abort
from pecan import hooks
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import router
from neutron import manager
class ResourceIdentifierHook(hooks.PecanHook):
priority = 95
def before(self, state):
# TODO(kevinbenton): find a better way to look this up. maybe something
# in the pecan internals somewhere?
# TODO(salv-orlando): try and leverage _lookup to this aim. Also remove
# the "special" code path for "actions"
state.request.resource_type = None
try:
# TODO(blogan): remove this dirty hack and do a better solution
# needs to work with /v2.0, /v2.0/ports, and /v2.0/ports.json
uri = state.request.path
if not uri.endswith('.json'):
uri += '.json'
# Remove the format suffix if any
uri = uri.rsplit('.', 1)[0].split('/')[2:]
if not uri:
# there's nothing to process in the URI
return
except IndexError:
return
resource_type = uri[0]
if resource_type == 'extensions':
return
for plural, single in attributes.PLURALS.items():
if plural == resource_type:
state.request.resource_type = single
state.request.plugin = self._plugin_for_resource(single)
state.request.member_action = self._parse_action(
single, plural, uri[1:])
return
abort(404, detail='Resource: %s' % resource_type)
def _parse_action(self, resource, collection, remainder):
# NOTE(salv-orlando): This check is revolting and makes me
# puke, but avoids silly failures when dealing with API actions
# such as "add_router_interface".
if len(remainder) > 1:
action = remainder[1]
else:
return
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
resource_exts = ext_mgr.get_resources()
for ext in resource_exts:
if (ext.collection == collection and
action in ext.member_actions):
return action
# Action or resource extension not found
if action:
abort(404, detail="Action %(action)s for resource "
"%(resource)s undefined" %
{'action': action,
'resource': resource})
def _plugin_for_resource(self, resource):
# NOTE(kevinbenton): memoizing the responses to this had no useful
# performance improvement so I avoided it to keep complexity and
# risks of memory leaks low.
if resource in router.RESOURCES:
# this is a core resource, return the core plugin
return manager.NeutronManager.get_plugin()
try:
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
# find the plugin that supports this extension
# TODO(kevinbenton): fix this. it incorrectly assumes the alias
# matches the resource. need to walk extensions and build map
for plugin in ext_mgr.plugins.values():
if (hasattr(plugin, 'supported_extension_aliases') and
resource in plugin.supported_extension_aliases):
return plugin
except KeyError:
pass
abort(404, detail='Resource: %s' % resource)

View File

@ -0,0 +1,38 @@
# 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_log import log as logging
from pecan import hooks
import webob.exc
from neutron.api.v2 import base as v2base
LOG = logging.getLogger(__name__)
class ExceptionTranslationHook(hooks.PecanHook):
def on_error(self, state, e):
# if it's already an http error, just return to let it go through
if isinstance(e, webob.exc.WSGIHTTPException):
return
for exc_class, to_class in v2base.FAULT_MAP.items():
if isinstance(e, exc_class):
raise to_class(getattr(e, 'msg', e.message))
# leaked unexpected exception, convert to boring old 500 error and
# hide message from user in case it contained sensitive details
LOG.exception(_("An unexpected exception was caught: %s") % e)
raise webob.exc.HTTPInternalServerError(
_("An unexpected internal error occured."))

View File

@ -0,0 +1,116 @@
# 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_log import log
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import router
from neutron.i18n import _LI, _LW
from neutron import manager
from neutron.pecan_wsgi.controllers import root
from neutron import policy
LOG = log.getLogger(__name__)
def _plugin_for_resource(collection):
if collection in router.RESOURCES.values():
# this is a core resource, return the core plugin
return manager.NeutronManager.get_plugin()
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
# Multiple extensions can map to the same resource. This happens
# because of 'attribute' extensions. Due to the way in which neutron
# plugins and request dispatching is constructed, it is impossible for
# the same resource to be handled by more than one plugin. Therefore
# all the extensions mapped to a given resource will necessarily be
# implemented by the same plugin.
ext_res_mappings = dict((ext.get_alias(), collection) for
ext in ext_mgr.extensions.values() if
collection in ext.get_extended_resources('2.0'))
LOG.debug("Extension mappings for: %(collection)s: %(aliases)s",
{'collection': collection, 'aliases': ext_res_mappings.keys()})
# find the plugin that supports this extension
for plugin in ext_mgr.plugins.values():
ext_aliases = getattr(plugin, 'supported_extension_aliases', [])
for alias in ext_aliases:
if alias in ext_res_mappings:
# This plugin implements this resource
return plugin
LOG.warn(_LW("No plugin found for:%s"), collection)
def _handle_plurals(collection):
resource = attributes.PLURALS.get(collection)
if not resource:
if collection.endswith('ies'):
resource = "%sy" % collection[:-3]
else:
resource = collection[:-1]
attributes.PLURALS[collection] = resource
return resource
def initialize_all():
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
ext_mgr.extend_resources("2.0", attributes.RESOURCE_ATTRIBUTE_MAP)
# At this stage we have a fully populated resource attribute map;
# build Pecan controllers and routes for every resource (both core
# and extensions)
pecanized_exts = [ext for ext in ext_mgr.extensions.values() if
hasattr(ext, 'get_pecan_controllers')]
pecan_controllers = {}
for ext in pecanized_exts:
LOG.debug("Extension %s is pecan-enabled. Fetching resources "
"and controllers", ext.get_name())
controllers = ext.get_pecan_controllers()
# controllers is actually a list of pairs where the first element is
# the collection name and the second the actual controller
for (collection, coll_controller) in controllers:
pecan_controllers[collection] = coll_controller
for collection in attributes.RESOURCE_ATTRIBUTE_MAP:
if collection not in pecan_controllers:
resource = _handle_plurals(collection)
LOG.debug("Building controller for resource:%s", resource)
plugin = _plugin_for_resource(collection)
if plugin:
manager.NeutronManager.set_plugin_for_resource(
resource, plugin)
controller = root.CollectionsController(collection, resource)
manager.NeutronManager.set_controller_for_resource(
collection, controller)
LOG.info(_LI("Added controller for resource %(resource)s "
"via URI path segment:%(collection)s"),
{'resource': resource,
'collection': collection})
else:
LOG.debug("There are already controllers for resource:%s",
resource)
for ext in ext_mgr.extensions.values():
# make each extension populate its plurals
if hasattr(ext, 'get_resources'):
ext.get_resources()
if hasattr(ext, 'get_extended_resources'):
ext.get_extended_resources('v2.0')
# Certain policy checks require that the extensions are loaded
# and the RESOURCE_ATTRIBUTE_MAP populated before they can be
# properly initialized. This can only be claimed with certainty
# once this point in the code has been reached. In the event
# that the policies have been initialized before this point,
# calling reset will cause the next policy check to
# re-initialize with all of the required data in place.
policy.reset()

View File

@ -146,17 +146,12 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
self.type_manager.initialize()
self.extension_manager.initialize()
self.mechanism_manager.initialize()
self._setup_rpc()
self._setup_dhcp()
self._start_rpc_notifiers()
LOG.info(_LI("Modular L2 Plugin initialization complete"))
def _setup_rpc(self):
"""Initialize components to support agent communication."""
self.notifier = rpc.AgentNotifierApi(topics.AGENT)
self.agent_notifiers[const.AGENT_TYPE_DHCP] = (
dhcp_rpc_agent_api.DhcpAgentNotifyAPI()
)
self.endpoints = [
rpc.RpcCallbacks(self.notifier, self.type_manager),
securitygroups_rpc.SecurityGroupServerRpcCallback(),
@ -178,9 +173,18 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
def supported_qos_rule_types(self):
return self.mechanism_manager.supported_qos_rule_types
@log_helpers.log_method_call
def _start_rpc_notifiers(self):
"""Initialize RPC notifiers for agents."""
self.notifier = rpc.AgentNotifierApi(topics.AGENT)
self.agent_notifiers[const.AGENT_TYPE_DHCP] = (
dhcp_rpc_agent_api.DhcpAgentNotifyAPI()
)
@log_helpers.log_method_call
def start_rpc_listeners(self):
"""Start the RPC loop to let the plugin communicate with agents."""
self._setup_rpc()
self.topic = topics.PLUGIN
self.conn = n_rpc.create_connection(new=True)
self.conn.create_consumer(self.topic, self.endpoints, fanout=False)

View File

@ -18,47 +18,21 @@
import sys
import eventlet
from oslo_config import cfg
from oslo_log import log as logging
from neutron.common import config
from neutron.i18n import _LI
from neutron import service
LOG = logging.getLogger(__name__)
def main():
def boot_server(server_func):
# the configuration will be read into the cfg.CONF global data structure
config.init(sys.argv[1:])
config.setup_logging()
if not cfg.CONF.config_file:
sys.exit(_("ERROR: Unable to find configuration file via the default"
" search paths (~/.neutron/, ~/, /etc/neutron/, /etc/) and"
" the '--config-file' option!"))
try:
pool = eventlet.GreenPool()
neutron_api = service.serve_wsgi(service.NeutronApiService)
api_thread = pool.spawn(neutron_api.wait)
try:
neutron_rpc = service.serve_rpc()
except NotImplementedError:
LOG.info(_LI("RPC was already started in parent process by "
"plugin."))
else:
rpc_thread = pool.spawn(neutron_rpc.wait)
plugin_workers = service.start_plugin_workers()
for worker in plugin_workers:
pool.spawn(worker.wait)
# api and rpc should die together. When one dies, kill the other.
rpc_thread.link(lambda gt: api_thread.kill())
api_thread.link(lambda gt: rpc_thread.kill())
pool.waitall()
server_func()
except KeyboardInterrupt:
pass
except RuntimeError as e:

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python
# Copyright 2011 VMware, 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.
# If ../neutron/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
import eventlet
from oslo_log import log
from neutron.i18n import _LI
from neutron import server
from neutron import service
LOG = log.getLogger(__name__)
def _eventlet_rpc_server():
pool = eventlet.GreenPool()
LOG.info(_LI("Eventlet based AMQP RPC server starting..."))
try:
neutron_rpc = service.serve_rpc()
except NotImplementedError:
LOG.info(_LI("RPC was already started in parent process by "
"plugin."))
else:
pool.spawn(neutron_rpc.wait)
pool.waitall()
def main():
server.boot_server(_eventlet_rpc_server)

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python
# 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 eventlet
from oslo_log import log
from neutron.i18n import _LI
from neutron import server
from neutron import service
LOG = log.getLogger(__name__)
def _eventlet_wsgi_server():
pool = eventlet.GreenPool()
neutron_api = service.serve_wsgi(service.NeutronApiService)
api_thread = pool.spawn(neutron_api.wait)
try:
neutron_rpc = service.serve_rpc()
except NotImplementedError:
LOG.info(_LI("RPC was already started in parent process by "
"plugin."))
else:
rpc_thread = pool.spawn(neutron_rpc.wait)
plugin_workers = service.start_plugin_workers()
for worker in plugin_workers:
pool.spawn(worker.wait)
# api and rpc should die together. When one dies, kill the other.
rpc_thread.link(lambda gt: api_thread.kill())
api_thread.link(lambda gt: rpc_thread.kill())
pool.waitall()
def main():
server.boot_server(_eventlet_wsgi_server)

60
neutron/server/wsgi_pecan.py Executable file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env python
# 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 logging as std_logging
from wsgiref import simple_server
from oslo_config import cfg
from oslo_log import log
from six.moves import socketserver
from neutron.common import rpc as n_rpc
from neutron.i18n import _LI, _LW
from neutron.pecan_wsgi import app as pecan_app
from neutron import server
LOG = log.getLogger(__name__)
class ThreadedSimpleServer(socketserver.ThreadingMixIn,
simple_server.WSGIServer):
pass
def _pecan_wsgi_server():
LOG.info(_LI("Pecan WSGI server starting..."))
# No AMQP connection should be created within this process
n_rpc.RPC_DISABLED = True
application = pecan_app.setup_app()
host = cfg.CONF.bind_host
port = cfg.CONF.bind_port
wsgi = simple_server.make_server(
host,
port,
application,
server_class=ThreadedSimpleServer
)
# Log option values
cfg.CONF.log_opt_values(LOG, std_logging.DEBUG)
LOG.warning(
_LW("Development Server Serving on http://%(host)s:%(port)s"),
{'host': host, 'port': port}
)
wsgi.serve_forever()
def main():
server.boot_server(_pecan_wsgi_server)

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.pecan_wsgi.controllers.root.RootController',
'modules': ['neutron.pecan_wsgi'],
'errors': {
400: '/error',
'__force_dict__': True
}
}

View File

@ -0,0 +1,268 @@
# 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
import mock
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from pecan import request
from pecan import set_config
from pecan.testing import load_test_app
import testtools
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.common import exceptions as n_exc
from neutron import context
from neutron import manager
from neutron.pecan_wsgi.controllers import root as controllers
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(extensions.PluginAwareExtensionManager.clear_instance)
self.addCleanup(set_config, {}, overwrite=True)
self.set_config_overrides()
self.setup_app()
def setup_app(self):
self.app = load_test_app(os.path.join(
os.path.dirname(__file__),
'config.py'
))
self._gen_port()
def _gen_port(self):
pl = manager.NeutronManager.get_plugin()
network_id = pl.create_network(context.get_admin_context(), {
'network':
{'name': 'pecannet', 'tenant_id': 'tenid', 'shared': False,
'admin_state_up': True, 'status': 'ACTIVE'}})['id']
self.port = pl.create_port(context.get_admin_context(), {
'port':
{'tenant_id': 'tenid', 'network_id': network_id,
'fixed_ips': attributes.ATTR_NOT_SPECIFIED,
'mac_address': '00:11:22:33:44:55',
'admin_state_up': True, 'device_id': 'FF',
'device_owner': 'pecan', 'name': 'pecan'}})
def set_config_overrides(self):
cfg.CONF.set_override('auth_strategy', 'noauth')
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': {'network_id': self.port['network_id'],
'admin_state_up': True,
'tenant_id': 'tenid'}},
headers={'X-Tenant-Id': 'tenid'})
self.assertEqual(response.status_int, 201)
def test_put(self):
response = self.app.put_json('/v2.0/ports/%s.json' % self.port['id'],
params={'port': {'name': 'test'}},
headers={'X-Tenant-Id': 'tenid'})
self.assertEqual(response.status_int, 200)
def test_delete(self):
response = self.app.delete('/v2.0/ports/%s.json' % self.port['id'],
headers={'X-Tenant-Id': 'tenid'})
self.assertEqual(response.status_int, 204)
def test_plugin_initialized(self):
self.assertIsNotNone(manager.NeutronManager._instance)
def test_get_extensions(self):
response = self.app.get('/v2.0/extensions.json')
self.assertEqual(response.status_int, 200)
def test_get_specific_extension(self):
response = self.app.get('/v2.0/extensions/allowed-address-pairs.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/ports/44.json',
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))
class TestKeystoneAuth(PecanFunctionalTest):
def set_config_overrides(self):
# default auth strategy is keystone so we pass
pass
def test_auth_enforced(self):
response = self.app.get('/', expect_errors=True)
self.assertEqual(response.status_int, 401)
class TestInvalidAuth(PecanFunctionalTest):
def setup_app(self):
# disable normal app setup since it will fail
pass
def test_invalid_auth_strategy(self):
cfg.CONF.set_override('auth_strategy', 'badvalue')
with testtools.ExpectedException(n_exc.InvalidConfigurationOption):
load_test_app(os.path.join(os.path.dirname(__file__), 'config.py'))
class TestExceptionTranslationHook(PecanFunctionalTest):
def test_neutron_nonfound_to_webob_exception(self):
# this endpoint raises a Neutron notfound exception. make sure it gets
# translated into a 404 error
with mock.patch(
'neutron.pecan_wsgi.controllers.root.CollectionsController.get',
side_effect=n_exc.NotFound()
):
response = self.app.get('/v2.0/ports.json', expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_unexpected_exception(self):
with mock.patch(
'neutron.pecan_wsgi.controllers.root.CollectionsController.get',
side_effect=ValueError('secretpassword')
):
response = self.app.get('/v2.0/ports.json', expect_errors=True)
self.assertNotIn(response.body, 'secretpassword')
self.assertEqual(response.status_int, 500)
class TestRequestPopulatingHooks(PecanFunctionalTest):
def setUp(self):
super(TestRequestPopulatingHooks, self).setUp()
# request.context is thread-local storage so it has to be accessed by
# the controller. We can capture it into a list here to assert on after
# the request finishes.
def capture_request_details(*args, **kwargs):
self.req_stash = {
'context': request.context['neutron_context'],
'resource_type': request.context['resource'],
}
mock.patch(
'neutron.pecan_wsgi.controllers.root.CollectionsController.get',
side_effect=capture_request_details
).start()
# TODO(kevinbenton): add context tests for X-Roles etc
def test_context_set_in_request(self):
self.app.get('/v2.0/ports.json',
headers={'X-Tenant-Id': 'tenant_id'})
self.assertEqual('tenant_id', self.req_stash['context'].tenant_id)
def test_core_resource_identified(self):
self.app.get('/v2.0/ports.json')
self.assertEqual('port', self.req_stash['resource_type'])
def test_service_plugin_identified(self):
# TODO(kevinbenton): fix the unit test setup to include an l3 plugin
self.skipTest("A dummy l3 plugin needs to be setup")
self.app.get('/v2.0/routers.json')
self.assertEqual('router', self.req_stash['resource_type'])
# make sure the core plugin was identified as the handler for ports
self.assertEqual(
manager.NeutronManager.get_service_plugins()['L3_ROUTER_NAT'],
self.req_stash['plugin'])
class TestEnforcementHooks(PecanFunctionalTest):
def test_network_ownership_check(self):
# TODO(kevinbenton): get a scenario that passes attribute population
self.skipTest("Attribute population blocks this test as-is")
response = self.app.post_json('/v2.0/ports.json',
params={'port': {'network_id': self.port['network_id'],
'admin_state_up': True,
'tenant_id': 'tenid2'}},
headers={'X-Tenant-Id': 'tenid'})
self.assertEqual(response.status_int, 200)
def test_quota_enforcement(self):
# TODO(kevinbenton): this test should do something
pass
def test_policy_enforcement(self):
# TODO(kevinbenton): this test should do something
pass
class TestRootController(PecanFunctionalTest):
"""Test version listing on root URI."""
def test_get(self):
response = self.app.get('/')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
versions = json_body.get('versions')
self.assertEqual(1, len(versions))
for (attr, value) in controllers.V2Controller.version_info.items():
self.assertIn(attr, versions[0])
self.assertEqual(value, versions[0][attr])
def _test_method_returns_405(self, method):
api_method = getattr(self.app, method)
response = api_method('/', expect_errors=True)
self.assertEqual(response.status_int, 405)
def test_post(self):
self._test_method_returns_405('post')
def test_put(self):
self._test_method_returns_405('put')
def test_patch(self):
self._test_method_returns_405('patch')
def test_delete(self):
self._test_method_returns_405('delete')
def test_head(self):
self._test_method_returns_405('head')

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>=1.0.0
greenlet>=0.3.2
httplib2>=0.7.5
requests>=2.5.2

View File

@ -93,7 +93,9 @@ console_scripts =
neutron-ovs-cleanup = neutron.cmd.ovs_cleanup:main
neutron-pd-notify = neutron.cmd.pd_notify:main
neutron-restproxy-agent = neutron.plugins.bigswitch.agent.restproxy_agent:main
neutron-server = neutron.cmd.eventlet.server:main
neutron-server = neutron.cmd.eventlet.server:main_wsgi_eventlet
neutron-dev-server = neutron.cmd.eventlet.server:main_wsgi_pecan
neutron-rpc-server = neutron.cmd.eventlet.server:main_rpc_eventlet
neutron-rootwrap = oslo_rootwrap.cmd:main
neutron-rootwrap-daemon = oslo_rootwrap.cmd:daemon
neutron-usage-audit = neutron.cmd.usage_audit: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 server; server.main_wsgi_pecan()'"
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