Merge remote-tracking branch 'origin/feature/pecan' into merge-branch
Change-Id: I1c0b6a9837273f6a5a24f537d75c1ff43ff599f8
This commit is contained in:
commit
e95904060a
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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 []
|
|
@ -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
|
|
@ -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})
|
|
@ -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
|
|
@ -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,
|
||||
})
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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."))
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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')
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue