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:
parent
b3e7809421
commit
cbae3e0de0
|
@ -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 = []
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue