From 2c40310584826c69fbed01a3f31fb2a2dbc5243f Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Fri, 14 Aug 2015 16:32:38 -0700 Subject: [PATCH] Use pecan controllers for routing Pecan defines several efficient mechanism for routing requests to the appropriate controller, but the current code for Neutron's Pecan WSGI server basically uses Pecan hooks to route requests. This patch partially fixes that, removing the 'resource_identifier' pecan hook and replacing it with explicit pecan routes between controllers added at resource registration time. All the remaining hooks, like attribute_population and policy_enforments, which were relying on finding the resource name in the pecan.request threadlocal variable have been updated. This patch also: - ensures the appropriate plugin is always selected for a given resource - add a common NeutronPecanController base class for the classes CollectionsController and ItemaController - Fixes the way in which plurals and singulars are handled in neutron.api.v2.resource_heper Change-Id: I4ec0d2276c3974117b497228d289c3fb0dc5a140 --- neutron/api/extensions.py | 4 + neutron/api/v2/resource_helper.py | 12 ++- neutron/manager.py | 19 +++++ neutron/pecan_wsgi/app.py | 2 +- neutron/pecan_wsgi/controllers/root.py | 71 +++++++++++------ neutron/pecan_wsgi/hooks/__init__.py | 4 +- .../pecan_wsgi/hooks/attribute_population.py | 29 ++++--- neutron/pecan_wsgi/hooks/context.py | 2 +- neutron/pecan_wsgi/hooks/member_action.py | 68 ++++++++++++++++ .../pecan_wsgi/hooks/ownership_validation.py | 15 ++-- .../pecan_wsgi/hooks/policy_enforcement.py | 60 +++++++------- neutron/pecan_wsgi/hooks/quota_enforcement.py | 26 ++++-- neutron/pecan_wsgi/startup.py | 79 +++++++++++++++++++ .../functional/pecan_wsgi/test_functional.py | 10 +-- 14 files changed, 310 insertions(+), 91 deletions(-) create mode 100644 neutron/pecan_wsgi/hooks/member_action.py diff --git a/neutron/api/extensions.py b/neutron/api/extensions.py index 86b5f9871eb..4af58774ac9 100644 --- a/neutron/api/extensions.py +++ b/neutron/api/extensions.py @@ -601,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() diff --git a/neutron/api/v2/resource_helper.py b/neutron/api/v2/resource_helper.py index c506320c91d..bbdc2a110e9 100644 --- a/neutron/api/v2/resource_helper.py +++ b/neutron/api/v2/resource_helper.py @@ -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 diff --git a/neutron/manager.py b/neutron/manager.py index 7a174507fdd..a2f22b16fa5 100644 --- a/neutron/manager.py +++ b/neutron/manager.py @@ -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) diff --git a/neutron/pecan_wsgi/app.py b/neutron/pecan_wsgi/app.py index a369c834a72..77d4c44a249 100644 --- a/neutron/pecan_wsgi/app.py +++ b/neutron/pecan_wsgi/app.py @@ -44,7 +44,7 @@ def setup_app(*args, **kwargs): app_hooks = [ hooks.ExceptionTranslationHook(), # priority 100 hooks.ContextHook(), # priority 95 - hooks.ResourceIdentifierHook(), # priority 95 + hooks.MemberActionHook(), # piority 95 hooks.AttributePopulationHook(), # priority 120 hooks.OwnershipValidationHook(), # priority 125 hooks.QuotaEnforcementHook(), # priority 130 diff --git a/neutron/pecan_wsgi/controllers/root.py b/neutron/pecan_wsgi/controllers/root.py index 8a8e725f6f5..4e69beeec8d 100644 --- a/neutron/pecan_wsgi/controllers/root.py +++ b/neutron/pecan_wsgi/controllers/root.py @@ -14,12 +14,16 @@ # 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 = {} @@ -99,8 +103,17 @@ class V2Controller(object): pecan.abort(405) @expose() - def _lookup(self, endpoint, *remainder): - return CollectionsController(endpoint), remainder + 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 @@ -124,14 +137,20 @@ class ExtensionController(object): return {'extension': extensions.ExtensionController._translate(ext)} -class CollectionsController(object): +class NeutronPecanController(object): - def __init__(self, collection): + 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(item), remainder + return ItemController(self.resource, item), remainder @expose(generic=True) def index(self, *args, **kwargs): @@ -145,25 +164,28 @@ class CollectionsController(object): _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(request.plugin, 'get_%s' % self.collection) - return {self.collection: lister(request.context, filters=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' % request.resource_type + method = 'create_%s_bulk' % self.resource else: - method = 'create_%s' % request.resource_type - creator = getattr(request.plugin, method) - key = self.collection if request.bulk else request.resource_type - return {key: creator(request.context, request.prepared_data)} + 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(object): +class ItemController(NeutronPecanController): - def __init__(self, item): + def __init__(self, resource, item): + super(ItemController, self).__init__(None, resource) self.item = item @expose(generic=True) @@ -171,23 +193,26 @@ class ItemController(object): return self.get() def get(self, *args, **kwargs): - getter = getattr(request.plugin, 'get_%s' % request.resource_type) - return {request.resource_type: getter(request.context, self.item)} + 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(request.plugin, + member_action_method = getattr(self.plugin, request.member_action) - return member_action_method(request.context, self.item, + return member_action_method(neutron_context, self.item, request.prepared_data) - updater = getattr(request.plugin, 'update_%s' % request.resource_type) - return {request.resource_type: updater( - request.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 - deleter = getattr(request.plugin, 'delete_%s' % request.resource_type) - return deleter(request.context, self.item) + neutron_context = request.context.get('neutron_context') + deleter = getattr(self.plugin, 'delete_%s' % self.resource) + return deleter(neutron_context, self.item) diff --git a/neutron/pecan_wsgi/hooks/__init__.py b/neutron/pecan_wsgi/hooks/__init__.py index cfe844c6e39..7ac38da781f 100644 --- a/neutron/pecan_wsgi/hooks/__init__.py +++ b/neutron/pecan_wsgi/hooks/__init__.py @@ -15,17 +15,17 @@ 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 resource_identifier from neutron.pecan_wsgi.hooks import translation ExceptionTranslationHook = translation.ExceptionTranslationHook ContextHook = context.ContextHook -ResourceIdentifierHook = resource_identifier.ResourceIdentifierHook +MemberActionHook = member_action.MemberActionHook AttributePopulationHook = attribute_population.AttributePopulationHook OwnershipValidationHook = ownership_validation.OwnershipValidationHook PolicyHook = policy_enforcement.PolicyHook diff --git a/neutron/pecan_wsgi/hooks/attribute_population.py b/neutron/pecan_wsgi/hooks/attribute_population.py index d85eea58624..311a1580fb4 100644 --- a/neutron/pecan_wsgi/hooks/attribute_population.py +++ b/neutron/pecan_wsgi/hooks/attribute_population.py @@ -17,6 +17,7 @@ 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): @@ -29,7 +30,8 @@ class AttributePopulationHook(hooks.PecanHook): if state.request.method not in ('POST', 'PUT'): return is_create = state.request.method == 'POST' - resource = state.request.resource_type + resource = state.request.context.get('resource') + neutron_context = state.request.context['neutron_context'] if not resource: return if state.request.member_action: @@ -41,23 +43,24 @@ class AttributePopulationHook(hooks.PecanHook): else: state.request.prepared_data = ( v2base.Controller.prepare_request_body( - state.request.context, state.request.json, is_create, + 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) + 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 = state.request.plugin + 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(state.request.context, obj_id, fields=field_list) + obj = getter(neutron_context, obj_id, fields=field_list) state.request.original_object = obj @@ -68,11 +71,11 @@ def _attributes_for_resource(resource): _plural(resource), {}) -def _pull_id_from_request(request): +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(request.resource_type) + resources = _plural(resource) jsontrail = request.path_info.replace('/v2.0/%s/' % resources, '') obj_id = jsontrail.replace('.json', '') return obj_id @@ -85,17 +88,17 @@ def _plural(rtype): def _extract_resources_from_state(state): - resource_type = state.request.resource_type - if not resource_type: + resource = state.request.context['resource'] + if not resource: return [] data = state.request.prepared_data # single item - if resource_type in data: + if resource in data: state.request.bulk = False - return [data[resource_type]] + return [data[resource]] # multiple items - if _plural(resource_type) in data: + if _plural(resource) in data: state.request.bulk = True - return [x[resource_type] for x in data[_plural(resource_type)]] + return data[_plural(resource)] return [] diff --git a/neutron/pecan_wsgi/hooks/context.py b/neutron/pecan_wsgi/hooks/context.py index 31369249637..5aaa2e84642 100644 --- a/neutron/pecan_wsgi/hooks/context.py +++ b/neutron/pecan_wsgi/hooks/context.py @@ -55,4 +55,4 @@ class ContextHook(hooks.PecanHook): request_id=req_id, auth_token=auth_token) # Inject the context... - state.request.context = ctx + state.request.context['neutron_context'] = ctx diff --git a/neutron/pecan_wsgi/hooks/member_action.py b/neutron/pecan_wsgi/hooks/member_action.py new file mode 100644 index 00000000000..86cfd3f0b98 --- /dev/null +++ b/neutron/pecan_wsgi/hooks/member_action.py @@ -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}) diff --git a/neutron/pecan_wsgi/hooks/ownership_validation.py b/neutron/pecan_wsgi/hooks/ownership_validation.py index 73ba8d0bcc7..e6449acfbef 100644 --- a/neutron/pecan_wsgi/hooks/ownership_validation.py +++ b/neutron/pecan_wsgi/hooks/ownership_validation.py @@ -28,17 +28,18 @@ class OwnershipValidationHook(hooks.PecanHook): return items = state.request.resources for item in items: - self._validate_network_tenant_ownership(state.request, item) + self._validate_network_tenant_ownership(state, item) - def _validate_network_tenant_ownership(self, request, resource_item): + def _validate_network_tenant_ownership(self, state, resource_item): # TODO(salvatore-orlando): consider whether this check can be folded # in the policy engine - rtype = request.resource_type - if (request.context.is_admin or request.context.is_advsvc or - rtype not in ('port', 'subnet')): + 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(request.context, + network = plugin.get_network(neutron_context, resource_item['network_id']) # do not perform the check on shared networks if network.get('shared'): @@ -51,5 +52,5 @@ class OwnershipValidationHook(hooks.PecanHook): "create %(resource)s on this network") raise webob.exc.HTTPForbidden(msg % { "tenant_id": resource_item['tenant_id'], - "resource": rtype, + "resource": resource, }) diff --git a/neutron/pecan_wsgi/hooks/policy_enforcement.py b/neutron/pecan_wsgi/hooks/policy_enforcement.py index c55096fc9ef..0656e12144b 100644 --- a/neutron/pecan_wsgi/hooks/policy_enforcement.py +++ b/neutron/pecan_wsgi/hooks/policy_enforcement.py @@ -16,6 +16,7 @@ import copy import simplejson +from oslo_log import log from oslo_policy import policy as oslo_policy from oslo_utils import excutils import pecan @@ -23,9 +24,12 @@ 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 @@ -35,13 +39,12 @@ class PolicyHook(hooks.PecanHook): def before(self, state): if state.request.method not in self.ACTION_MAP: pecan.abort(405) - rtype = state.request.resource_type - if not rtype: - return + 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], rtype) + action = '%s_%s' % (self.ACTION_MAP[state.request.method], resource) for item in items: if is_update: obj = copy.copy(state.request.original_object) @@ -49,57 +52,60 @@ class PolicyHook(hooks.PecanHook): obj[const.ATTRIBUTES_TO_UPDATE] = item.keys() item = obj try: - policy.enforce(state.request.context, action, item, - pluralized=attribute_population._plural(rtype)) + 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. - context = state.request.context if (is_update and - context.tenant_id != obj['tenant_id']): + 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): - resource_type = getattr(state.request, 'resource_type', None) - if not resource_type: + 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_type == 'extension': + if resource == 'extension': return try: data = state.response.json except simplejson.JSONDecodeError: return action = '%s_%s' % (self.ACTION_MAP[state.request.method], - resource_type) - plural = attribute_population._plural(resource_type) - if not data or (resource_type not in data and plural not in data): + resource) + plural = attribute_population._plural(resource) + if not data or (resource not in data and plural not in data): return - is_single = resource_type in data - key = resource_type if is_single else plural - to_process = [data[resource_type]] if is_single else data[plural] + 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 - resp = [self._get_filtered_item(state.request, resource_type, item) + 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(state.request.context, action, item, - plugin=state.request.plugin, + 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_type, data): - to_exclude = self._exclude_attributes_by_policy(request.context, - resource_type, 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): @@ -111,7 +117,7 @@ class PolicyHook(hooks.PecanHook): 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_type, data): + 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 @@ -121,16 +127,16 @@ class PolicyHook(hooks.PecanHook): attributes_to_exclude = [] for attr_name in data.keys(): attr_data = attribute_population._attributes_for_resource( - resource_type).get(attr_name) + 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_type, attr_name), + 'get_%s:%s' % (resource, attr_name), data, might_not_exist=True, - pluralized=attribute_population._plural(resource_type)): + pluralized=attribute_population._plural(resource)): # this attribute is visible, check next one continue # if the code reaches this point then either the policy check diff --git a/neutron/pecan_wsgi/hooks/quota_enforcement.py b/neutron/pecan_wsgi/hooks/quota_enforcement.py index a45e5cccde9..bc9e46d6344 100644 --- a/neutron/pecan_wsgi/hooks/quota_enforcement.py +++ b/neutron/pecan_wsgi/hooks/quota_enforcement.py @@ -13,12 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. -from neutron.common import exceptions -from neutron import quota - 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__) @@ -27,22 +30,29 @@ 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 - rtype = state.request.resource_type deltas = {} for item in items: tenant_id = item['tenant_id'] try: - count = quota.QUOTAS.count(state.request.context, rtype, - state.request.plugin, + 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 = {rtype: count + delta} + 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(state.request.context, tenant_id, + quota.QUOTAS.limit_check(neutron_context, tenant_id, **kwargs) diff --git a/neutron/pecan_wsgi/startup.py b/neutron/pecan_wsgi/startup.py index e2e3f7fa223..758ac258843 100644 --- a/neutron/pecan_wsgi/startup.py +++ b/neutron/pecan_wsgi/startup.py @@ -13,14 +13,93 @@ # 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'): diff --git a/neutron/tests/functional/pecan_wsgi/test_functional.py b/neutron/tests/functional/pecan_wsgi/test_functional.py index a3666277fe0..1082df82a64 100644 --- a/neutron/tests/functional/pecan_wsgi/test_functional.py +++ b/neutron/tests/functional/pecan_wsgi/test_functional.py @@ -24,6 +24,7 @@ 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 @@ -37,6 +38,7 @@ 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() @@ -181,9 +183,8 @@ class TestRequestPopulatingHooks(PecanFunctionalTest): def capture_request_details(*args, **kwargs): self.req_stash = { - 'context': request.context, - 'resource_type': request.resource_type, - 'plugin': request.plugin + 'context': request.context['neutron_context'], + 'resource_type': request.context['resource'], } mock.patch( 'neutron.pecan_wsgi.controllers.root.CollectionsController.get', @@ -200,9 +201,6 @@ class TestRequestPopulatingHooks(PecanFunctionalTest): def test_core_resource_identified(self): self.app.get('/v2.0/ports.json') self.assertEqual('port', self.req_stash['resource_type']) - # make sure the core plugin was identified as the handler for ports - self.assertEqual(manager.NeutronManager.get_plugin(), - self.req_stash['plugin']) def test_service_plugin_identified(self): # TODO(kevinbenton): fix the unit test setup to include an l3 plugin