]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Use pecan controllers for routing
authorSalvatore Orlando <salv.orlando@gmail.com>
Fri, 14 Aug 2015 23:32:38 +0000 (16:32 -0700)
committerSalvatore Orlando <salv.orlando@gmail.com>
Fri, 18 Sep 2015 13:00:14 +0000 (06:00 -0700)
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

14 files changed:
neutron/api/extensions.py
neutron/api/v2/resource_helper.py
neutron/manager.py
neutron/pecan_wsgi/app.py
neutron/pecan_wsgi/controllers/root.py
neutron/pecan_wsgi/hooks/__init__.py
neutron/pecan_wsgi/hooks/attribute_population.py
neutron/pecan_wsgi/hooks/context.py
neutron/pecan_wsgi/hooks/member_action.py [new file with mode: 0644]
neutron/pecan_wsgi/hooks/ownership_validation.py
neutron/pecan_wsgi/hooks/policy_enforcement.py
neutron/pecan_wsgi/hooks/quota_enforcement.py
neutron/pecan_wsgi/startup.py
neutron/tests/functional/pecan_wsgi/test_functional.py

index 86b5f9871ebdcb20c836c9ef0230fef7f28d594a..4af58774ac9d50dd981b5dc70e895c262d589a36 100644 (file)
@@ -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()
index c506320c91dedf90d76b129705f6e525c6a8c187..bbdc2a110e9470481038a8247dd6b90cb190d4e2 100644 (file)
@@ -28,13 +28,19 @@ LOG = logging.getLogger(__name__)
 def build_plural_mappings(special_mappings, resource_map):
     """Create plural to singular mapping for all resources.
 
-    Allows for special mappings to be provided, like policies -> policy.
+    Allows for special mappings to be provided, for particular cases..
     Otherwise, will strip off the last character for normal mappings, like
-    routers -> router.
+    routers -> router, unless the plural name ends with 'ies', in which
+    case the singular form will end with a 'y' (e.g.: policy/policies)
     """
     plural_mappings = {}
     for plural in resource_map:
-        singular = special_mappings.get(plural, plural[:-1])
+        singular = special_mappings.get(plural)
+        if not singular:
+            if plural.endswith('ies'):
+                singular = "%sy" % plural[:-3]
+            else:
+                singular = plural[:-1]
         plural_mappings[plural] = singular
     return plural_mappings
 
index 7a174507fddc8e5e75c796fc5d70f3b6580a442f..a2f22b16fa5471646cf2421d7d0a2787a0e950f3 100644 (file)
@@ -129,6 +129,9 @@ class NeutronManager(object):
         # the rest of service plugins
         self.service_plugins = {constants.CORE: self.plugin}
         self._load_service_plugins()
+        # Used by pecan WSGI
+        self.resource_plugin_mappings = {}
+        self.resource_controller_mappings = {}
 
     @staticmethod
     def load_class_for_provider(namespace, plugin_provider):
@@ -251,3 +254,19 @@ class NeutronManager(object):
     def get_unique_service_plugins(cls):
         service_plugins = cls.get_instance().service_plugins
         return tuple(weakref.proxy(x) for x in set(service_plugins.values()))
+
+    @classmethod
+    def set_plugin_for_resource(cls, resource, plugin):
+        cls.get_instance().resource_plugin_mappings[resource] = plugin
+
+    @classmethod
+    def get_plugin_for_resource(cls, resource):
+        return cls.get_instance().resource_plugin_mappings.get(resource)
+
+    @classmethod
+    def set_controller_for_resource(cls, resource, controller):
+        cls.get_instance().resource_controller_mappings[resource] = controller
+
+    @classmethod
+    def get_controller_for_resource(cls, resource):
+        return cls.get_instance().resource_controller_mappings.get(resource)
index a369c834a723cfed2a75bd27d5aa185e3f35d074..77d4c44a24914bbc8f2555a779e2195e2f62470d 100644 (file)
@@ -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
index 8a8e725f6f5f997d5e81d5ebd45e813c2c2d8d2f..4e69beeec8ddfb108284edc516130084410b3616 100644 (file)
 #    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)
index cfe844c6e39ab487266affd029ff551da1585d0b..7ac38da781f8dc16de3895cc6bf61d37602c4cab 100644 (file)
 
 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
index d85eea586241e7b8d5c2b13b9f645624cf5cdc1a..311a1580fb4b371baff9eb1153ed407ca17a69eb 100644 (file)
@@ -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 []
index 31369249637cc0f564574362f1532beeed304c56..5aaa2e84642098411b94676dadf946071d12358a 100644 (file)
@@ -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 (file)
index 0000000..86cfd3f
--- /dev/null
@@ -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})
index 73ba8d0bcc7b4d10d76d3842714079daff96d302..e6449acfbeff3adeb952dcb54da59337c80b7468 100644 (file)
@@ -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,
             })
index c55096fc9efde86ab17e93c4eccb407357f91f14..0656e12144b61961e8b6e79c5f0b0a87d8031a64 100644 (file)
@@ -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
index a45e5cccde9f1dcc55ad1450451c486516fa7a2f..bc9e46d6344de90c9725b517aa7e3d0c2e569901 100644 (file)
 #    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
 
-from oslo_log import log as logging
-from pecan import hooks
 
 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)
index e2e3f7fa223eae8e2f7731552a85f2c871a0f30c..758ac25884392666781065aef536c5d10171cb3e 100644 (file)
 #    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'):
index a3666277fe04b541f28d44367b175b6734548e08..1082df82a649e49cb80e21d85526322c911136c2 100644 (file)
@@ -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