Pecan: Backwards compatible/cleaner init

Previously the pecan controller creation was based on the
attributes.RESOURCE_ATTRIBUTE_MAP's keys after it had been fully
populated by all active extensions.  The problem with the problem
with this strategy is that those keys did not necessarily define
the actual URI path, and if any extensions ever had subresources
(.i.e. /qos/policies/{policy_id}/bandwidth_limit_rules), the
attributes.RESOURCE_ATTRIBUTE_MAP was never populated with those.
Getting those subresources would require some very unsightly code
and strategies.

What this does is it grabs the resources that every extension
returns in the get_resources method.  The get_resources method
returns a list of ResourceExtensions that do infact define the
URI, and does have a clear linkage to subresources.  With this
in mind, the pecan initializer can now translate what get_resources
returns into pecan controllers with the correct paths.

The subresource work still needs to be done and will be done in a
follow-up patch.  Also, there is an issue with the
auto-allocate-topology tests, but this too can be solved in
another patch so this review does not get bloated.

Change-Id: I3b42b4b603af97538f08b401d2740d3049276a43
This commit is contained in:
Brandon Logan 2016-05-11 01:46:52 -05:00
parent b3e7809421
commit cbae3e0de0
10 changed files with 134 additions and 158 deletions

View File

@ -180,6 +180,19 @@ class ExtensionDescriptor(object):
if extended_attrs:
attrs.update(extended_attrs)
def get_pecan_resources(self):
"""List of PecanResourceExtension extension objects.
Resources define new nouns, and are accessible through URLs.
The controllers associated with each instance of
extensions.ResourceExtension should be a subclass of
neutron.pecan_wsgi.controllers.utils.NeutronPecanController.
If a resource is defined in both get_resources and get_pecan_resources,
the resource defined in get_pecan_resources will take precedence.
"""
return []
class ActionExtensionController(wsgi.Controller):
@ -420,6 +433,19 @@ class ExtensionManager(object):
resources.extend(ext.get_resources())
return resources
def get_pecan_resources(self):
"""Returns a list of PecanResourceExtension objects."""
resources = []
for ext in self.extensions.values():
# TODO(blogan): this is being called because there are side effects
# that the get_resources method does, like registering plural
# mappings and quotas. The side effects that get_resources does
# should probably be moved to another extension method, but that
# should be done some other time.
ext.get_resources()
resources.extend(ext.get_pecan_resources())
return resources
def get_actions(self):
"""Returns a list of ActionExtension objects."""
actions = []

View File

@ -60,6 +60,14 @@ class Controller(object):
UPDATE = 'update'
DELETE = 'delete'
@property
def plugin(self):
return self._plugin
@property
def resource(self):
return self._resource
def __init__(self, plugin, collection, resource, attr_info,
allow_bulk=False, member_actions=None, parent=None,
allow_pagination=False, allow_sorting=False):

View File

@ -24,6 +24,7 @@ from neutron.api import extensions
from neutron.api.v2 import attributes as attr
from neutron.api.v2 import resource_helper
from neutron.conf import quota
from neutron import manager
from neutron.pecan_wsgi import controllers
from neutron.pecan_wsgi.controllers import utils as pecan_utils
from neutron.plugins.common import constants
@ -201,14 +202,16 @@ class L3(extensions.ExtensionDescriptor):
attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP)
@classmethod
def get_pecan_controllers(cls):
return [
pecan_utils.PecanResourceExtension(
ROUTERS, controllers.RoutersController()),
pecan_utils.PecanResourceExtension(
FLOATINGIPS, controllers.CollectionsController(
FLOATINGIPS, FLOATINGIP))
]
def get_pecan_resources(cls):
plugin = manager.NeutronManager.get_service_plugins()[
constants.L3_ROUTER_NAT]
router_controller = controllers.RoutersController()
fip_controller = controllers.CollectionsController(
FLOATINGIPS, FLOATINGIP)
return [pecan_utils.PecanResourceExtension(
ROUTERS, router_controller, plugin),
pecan_utils.PecanResourceExtension(
FLOATINGIPS, fip_controller, plugin)]
def get_extended_resources(self, version):
if version == "2.0":

View File

@ -21,6 +21,7 @@ import webob
from neutron._i18n import _
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron.api.v2 import resource
from neutron.common import constants as const
@ -161,9 +162,12 @@ class Quotasv2(extensions.ExtensionDescriptor):
collection_actions={'tenant': 'GET'})]
@classmethod
def get_pecan_controllers(cls):
def get_pecan_resources(cls):
# NOTE: quotas in PLURALS is needed because get_resources never sets it
attributes.PLURALS[RESOURCE_COLLECTION] = RESOURCE_NAME
# NOTE: plugin is not needed for quotas
return [pecan_utils.PecanResourceExtension(
RESOURCE_COLLECTION, controllers.QuotasController())]
RESOURCE_COLLECTION, controllers.QuotasController(), None)]
def get_extended_resources(self, version):
if version == "2.0":

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from collections import defaultdict
import weakref
from oslo_config import cfg
@ -128,6 +129,7 @@ class NeutronManager(object):
# Used by pecan WSGI
self.resource_plugin_mappings = {}
self.resource_controller_mappings = {}
self.path_prefix_resource_mappings = defaultdict(list)
@staticmethod
def load_class_for_provider(namespace, plugin_provider):
@ -265,6 +267,8 @@ class NeutronManager(object):
resource,
res_ctrl_mappings.get(resource.replace('-', '_')))
# TODO(blogan): This isn't used by anything else other than tests and
# probably should be removed
@classmethod
def get_service_plugin_by_path_prefix(cls, path_prefix):
service_plugins = cls.get_unique_service_plugins()
@ -272,3 +276,13 @@ class NeutronManager(object):
plugin_path_prefix = getattr(service_plugin, 'path_prefix', None)
if plugin_path_prefix and plugin_path_prefix == path_prefix:
return service_plugin
@classmethod
def add_resource_for_path_prefix(cls, resource, path_prefix):
resources = cls.get_instance().path_prefix_resource_mappings[
path_prefix].append(resource)
return resources
@classmethod
def get_resources_for_path_prefix(cls, path_prefix):
return cls.get_instance().path_prefix_resource_mappings[path_prefix]

View File

@ -88,7 +88,7 @@ class V2Controller(object):
# needs to be remapped.
# Example: https://neutron.endpoint/v2.0/lbaas/loadbalancers
if (remainder and
manager.NeutronManager.get_service_plugin_by_path_prefix(
manager.NeutronManager.get_resources_for_path_prefix(
collection)):
collection = remainder[0]
remainder = remainder[1:]

View File

@ -87,12 +87,13 @@ def when(index, *args, **kwargs):
class NeutronPecanController(object):
def __init__(self, collection, resource):
def __init__(self, collection, resource, plugin=None):
# Ensure dashes are always replaced with underscores
self.collection = collection and collection.replace('-', '_')
self.resource = resource and resource.replace('-', '_')
self._resource_info = api_attributes.get_collection_info(collection)
self._plugin = None
self._resource_info = api_attributes.get_collection_info(
self.collection)
self._plugin = plugin
@property
def plugin(self):
@ -166,6 +167,7 @@ class ShimCollectionsController(NeutronPecanController):
class PecanResourceExtension(object):
def __init__(self, collection, controller):
def __init__(self, collection, controller, plugin):
self.collection = collection
self.controller = controller
self.plugin = plugin

View File

@ -15,157 +15,74 @@
from oslo_log import log
from neutron._i18n import _LI, _LW
from neutron._i18n import _LW
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron.api.v2 import router
from neutron import manager
from neutron.pecan_wsgi.controllers import resource as res_ctrl
from neutron.pecan_wsgi.controllers import utils
from neutron import policy
from neutron.quota import resource_registry
from neutron import wsgi
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. These extensions may come from
# various plugins and only one of them is the primary one responsible
# for the resource while the others just append fields to the response
# (e.g. timestamps). So we have to find the plugin that supports the
# extension and has the getter for the collection.
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 = ext_mgr.get_plugin_supported_extension_aliases(plugin)
for alias in ext_aliases:
if (alias in ext_res_mappings and
hasattr(plugin, 'get_%s' % collection)):
# This plugin implements this resource
return plugin
LOG.warning(_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_legacy_extensions(legacy_extensions):
leftovers = []
for ext in legacy_extensions:
ext_resources = ext.get_resources()
for ext_resource in ext_resources:
controller = ext_resource.controller.controller
collection = ext_resource.collection
collection = collection.replace("-", "_")
resource = _handle_plurals(collection)
if manager.NeutronManager.get_plugin_for_resource(resource):
continue
# NOTE(blogan): It is possible that a plugin is tied to the
# collection rather than the resource. An example of this is
# the auto_allocated_topology extension. All other extensions
# created their legacy resources with the collection/plural form
# except auto_allocated_topology. Making that extension
# conform with the rest of extensions could invalidate this, but
# it's possible out of tree extensions did the same thing. Since
# the auto_allocated_topology resources have already been loaded
# we definitely don't want to load them up with shim controllers,
# so this will prevent that.
if manager.NeutronManager.get_plugin_for_resource(collection):
continue
# NOTE(blogan): Since this does not have a plugin, we know this
# extension has not been loaded and controllers for its resources
# have not been created nor set.
leftovers.append((collection, resource, controller))
# NOTE(blogan): at this point we have leftover extensions that never
# had a controller set which will force us to use shim controllers.
for leftover in leftovers:
shim_controller = utils.ShimCollectionsController(*leftover)
manager.NeutronManager.set_controller_for_resource(
shim_controller.collection, shim_controller)
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')]
non_pecanized_exts = set(ext_mgr.extensions.values()) - set(pecanized_exts)
pecan_controllers = {}
for ext in pecanized_exts:
LOG.info(_LI("Extension %s is pecan-aware. Fetching resources "
"and controllers"), ext.get_name())
controllers = ext.get_pecan_controllers()
# controllers should be a list of PecanResourceExtensions
for res_ext in controllers:
pecan_controllers[res_ext.collection] = res_ext.controller
for collection in attributes.RESOURCE_ATTRIBUTE_MAP:
resource = _handle_plurals(collection)
plugin = _plugin_for_resource(collection)
if plugin:
manager.NeutronManager.set_plugin_for_resource(
resource, plugin)
else:
LOG.warning(_LW("No plugin found for resource:%s. API calls "
"may not be correctly dispatched"), resource)
controller = pecan_controllers.get(collection)
if not controller:
LOG.debug("Building controller for resource:%s", resource)
controller = res_ctrl.CollectionsController(collection, resource)
else:
LOG.debug("There are already controllers for resource: %s",
resource)
manager.NeutronManager.set_controller_for_resource(
controller.collection, controller)
LOG.info(_LI("Added controller for resource %(resource)s "
"via URI path segment:%(collection)s"),
{'resource': resource,
'collection': collection})
initialize_legacy_extensions(non_pecanized_exts)
# NOTE(salv-orlando): If you are care about code quality, please read below
# Hackiness is strong with the piece of code below. It is used for
# populating resource plurals and registering resources with the quota
# engine, but the method it calls were not conceived with this aim.
# Therefore it only leverages side-effects from those methods. Moreover,
# as it is really not advisable to load an instance of
# neutron.api.v2.router.APIRouter just to register resources with the
# quota engine, core resources are explicitly registered here.
# TODO(salv-orlando): The Pecan WSGI support should provide its own
# solution to manage resource plurals and registration of resources with
# the quota engine
for resource in router.RESOURCES.keys():
# build Pecan controllers and routes for all core resources
for resource, collection in router.RESOURCES.items():
resource_registry.register_resource_by_name(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')
plugin = manager.NeutronManager.get_plugin()
new_controller = res_ctrl.CollectionsController(collection, resource)
manager.NeutronManager.set_controller_for_resource(
collection, new_controller)
manager.NeutronManager.set_plugin_for_resource(resource, plugin)
pecanized_resources = ext_mgr.get_pecan_resources()
for pec_res in pecanized_resources:
resource = attributes.PLURALS[pec_res.collection]
manager.NeutronManager.set_controller_for_resource(
pec_res.collection, pec_res.controller)
manager.NeutronManager.set_plugin_for_resource(
resource, pec_res.plugin)
# Now build Pecan Controllers and routes for all extensions
resources = ext_mgr.get_resources()
# Extensions controller is already defined, we don't need it.
resources.pop(0)
for ext_res in resources:
path_prefix = ext_res.path_prefix.strip('/')
collection = ext_res.collection
if manager.NeutronManager.get_controller_for_resource(collection):
# This is a collection that already has a pecan controller, we
# do not need to do anything else
continue
legacy_controller = getattr(ext_res.controller, 'controller',
ext_res.controller)
new_controller = None
if isinstance(legacy_controller, base.Controller):
resource = legacy_controller.resource
plugin = legacy_controller.plugin
new_controller = res_ctrl.CollectionsController(
collection, resource)
manager.NeutronManager.set_plugin_for_resource(resource, plugin)
if path_prefix:
manager.NeutronManager.add_resource_for_path_prefix(
collection, path_prefix)
elif isinstance(legacy_controller, wsgi.Controller):
new_controller = utils.ShimCollectionsController(
collection, None, legacy_controller)
else:
LOG.warning(_LW("Unknown controller type encountered %s. It will"
"be ignored."), legacy_controller)
manager.NeutronManager.set_controller_for_resource(
collection, new_controller)
# 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

View File

@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from collections import namedtuple
import mock
from neutron_lib import constants as n_const
from oslo_config import cfg
@ -425,10 +423,9 @@ class TestRequestProcessing(TestResourceController):
self.req_stash['plugin'])
def test_service_plugin_uri(self):
service_plugin = namedtuple('DummyServicePlugin', 'path_prefix')
service_plugin.path_prefix = 'dummy'
nm = manager.NeutronManager.get_instance()
nm.service_plugins['dummy_sp'] = service_plugin
nm.path_prefix_resource_mappings['dummy'] = [
_SERVICE_PLUGIN_COLLECTION]
response = self.do_request('/v2.0/dummy/serviceplugins.json')
self.assertEqual(200, response.status_int)
self.assertEqual(_SERVICE_PLUGIN_INDEX_BODY, response.json_body)
@ -610,7 +607,8 @@ class TestShimControllers(test_functional.PecanFunctionalTest):
self.addCleanup(policy.reset)
def test_hyphenated_resource_controller_not_shimmed(self):
collection = pecan_utils.FakeExtension.HYPHENATED_COLLECTION
collection = pecan_utils.FakeExtension.HYPHENATED_COLLECTION.replace(
'_', '-')
resource = pecan_utils.FakeExtension.HYPHENATED_RESOURCE
url = '/v2.0/{}/something.json'.format(collection)
resp = self.app.get(url)
@ -618,8 +616,9 @@ class TestShimControllers(test_functional.PecanFunctionalTest):
self.assertEqual({resource: {'fake': 'something'}}, resp.json)
def test_hyphenated_collection_controller_not_shimmed(self):
collection = pecan_utils.FakeExtension.HYPHENATED_COLLECTION
url = '/v2.0/{}.json'.format(collection)
body_collection = pecan_utils.FakeExtension.HYPHENATED_COLLECTION
uri_collection = body_collection.replace('_', '-')
url = '/v2.0/{}.json'.format(uri_collection)
resp = self.app.get(url)
self.assertEqual(200, resp.status_int)
self.assertEqual({collection: [{'fake': 'fake'}]}, resp.json)
self.assertEqual({body_collection: [{'fake': 'fake'}]}, resp.json)

View File

@ -11,6 +11,7 @@
# under the License.
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron.pecan_wsgi import controllers
from neutron.pecan_wsgi.controllers import utils as pecan_utils
@ -134,6 +135,8 @@ class FakeExtension(extensions.ExtensionDescriptor):
def get_resources(self):
collection = self.HYPHENATED_COLLECTION.replace('_', '-')
params = self.RAM.get(self.HYPHENATED_COLLECTION, {})
attributes.PLURALS.update({self.HYPHENATED_COLLECTION:
self.HYPHENATED_RESOURCE})
controller = base.create_resource(
collection, self.HYPHENATED_RESOURCE, FakePlugin(),
params, allow_bulk=True, allow_pagination=True,