From 9e87a70a1a163f42c65dad66ef617bbb8e479b50 Mon Sep 17 00:00:00 2001 From: tonytan4ever Date: Thu, 30 Jun 2016 14:29:16 -0400 Subject: [PATCH] Add subresources support for PECAN Implements adding a subresource (of extensions) to its parents A resource and a subresource has a parent-child relationship, for example: A qos resource has a subresource of bandwith_limit_rules. This patch will allow urls like : /qos/policies/{policy_id}/bandwidth_limit_rules to work. Change-Id: Ib2288234710fd6eed7fb9f6b880f57c9dd3beade --- neutron/pecan_wsgi/controllers/resource.py | 50 ++++++++++++++----- neutron/pecan_wsgi/controllers/utils.py | 5 ++ .../pecan_wsgi/hooks/policy_enforcement.py | 24 ++++++--- .../functional/pecan_wsgi/test_controllers.py | 21 +++++++- neutron/tests/functional/pecan_wsgi/utils.py | 43 +++++++++++++++- 5 files changed, 121 insertions(+), 22 deletions(-) diff --git a/neutron/pecan_wsgi/controllers/resource.py b/neutron/pecan_wsgi/controllers/resource.py index d4d6917c45c..dd48333236f 100644 --- a/neutron/pecan_wsgi/controllers/resource.py +++ b/neutron/pecan_wsgi/controllers/resource.py @@ -26,9 +26,11 @@ LOG = logging.getLogger(__name__) class ItemController(utils.NeutronPecanController): - def __init__(self, resource, item, plugin=None, resource_info=None): + def __init__(self, resource, item, plugin=None, resource_info=None, + parent_resource=None): super(ItemController, self).__init__(None, resource, plugin=plugin, - resource_info=resource_info) + resource_info=resource_info, + parent_resource=parent_resource) self.item = item @utils.expose(generic=True) @@ -37,9 +39,14 @@ class ItemController(utils.NeutronPecanController): def get(self, *args, **kwargs): neutron_context = request.context['neutron_context'] + getter_args = [neutron_context, self.item] + # NOTE(tonytan4ever): This implicitly forces the getter method + # uses the parent_id as the last argument, thus easy for future + # refactoring + if 'parent_id' in request.context: + getter_args.append(request.context['parent_id']) fields = request.context['query_params'].get('fields') - return {self.resource: self.plugin_shower(neutron_context, self.item, - fields=fields)} + return {self.resource: self.plugin_shower(*getter_args, fields=fields)} @utils.when(index, method='HEAD') @utils.when(index, method='POST') @@ -55,15 +62,21 @@ class ItemController(utils.NeutronPecanController): # Bulk update is not supported, 'resources' always contains a single # elemenet data = {self.resource: resources[0]} - return {self.resource: self.plugin_updater(neutron_context, - self.item, data)} + updater_args = [neutron_context, self.item] + if 'parent_id' in request.context: + updater_args.append(request.context['parent_id']) + updater_args.append(data) + return {self.resource: self.plugin_updater(*updater_args)} @utils.when(index, method='DELETE') def delete(self): # TODO(kevinbenton): setting code could be in a decorator pecan.response.status = 204 neutron_context = request.context['neutron_context'] - return self.plugin_deleter(neutron_context, self.item) + deleter_args = [neutron_context, self.item] + if 'parent_id' in request.context: + deleter_args.append(request.context['parent_id']) + return self.plugin_deleter(*deleter_args) @utils.expose() def _lookup(self, collection, *remainder): @@ -72,8 +85,10 @@ class ItemController(utils.NeutronPecanController): collection) if not controller: LOG.warning(_LW("No controller found for: %s - returning response " - "code 404"), collection) + "code 404"), collection) pecan.abort(404) + request.context['resource'] = controller.resource + request.context['parent_id'] = request.context['resource_id'] return controller, remainder @@ -88,7 +103,11 @@ class CollectionsController(utils.NeutronPecanController): uri_identifier = '%s_id' % self.resource request.context['uri_identifiers'][uri_identifier] = item return (self.item_controller_class( - self.resource, item, resource_info=self.resource_info), + self.resource, item, resource_info=self.resource_info, + # NOTE(tonytan4ever): item needs to share the same + # parent as collection + parent_resource=self.parent + ), remainder) @utils.expose(generic=True) @@ -96,10 +115,13 @@ class CollectionsController(utils.NeutronPecanController): return self.get(*args, **kwargs) def get(self, *args, **kwargs): - # NOTE(blogan): query_params is set in the QueryParametersHook + # NOTE(blogan): these are set in the FieldsAndFiltersHoook query_params = request.context['query_params'] neutron_context = request.context['neutron_context'] - return {self.collection: self.plugin_lister(neutron_context, + lister_args = [neutron_context] + if 'parent_id' in request.context: + lister_args.append(request.context['parent_id']) + return {self.collection: self.plugin_lister(*lister_args, **query_params)} @utils.when(index, method='HEAD') @@ -127,4 +149,8 @@ class CollectionsController(utils.NeutronPecanController): key = self.resource data = {key: resources[0]} neutron_context = request.context['neutron_context'] - return {key: creator(neutron_context, data)} + creator_args = [neutron_context] + if 'parent_id' in request.context: + creator_args.append(request.context['parent_id']) + creator_args.append(data) + return {key: creator(*creator_args)} diff --git a/neutron/pecan_wsgi/controllers/utils.py b/neutron/pecan_wsgi/controllers/utils.py index 63c19eac52b..e195711c53b 100644 --- a/neutron/pecan_wsgi/controllers/utils.py +++ b/neutron/pecan_wsgi/controllers/utils.py @@ -124,6 +124,7 @@ class NeutronPecanController(object): self.plugin) self.primary_key = self._get_primary_key() + self.parent = parent_resource parent_resource = '_%s' % parent_resource if parent_resource else '' self._parent_id_name = ('%s_id' % parent_resource if parent_resource else None) @@ -166,6 +167,10 @@ class NeutronPecanController(object): return key return default_primary_key + @property + def plugin_handlers(self): + return self._plugin_handlers + @property def plugin_lister(self): return getattr(self.plugin, self._plugin_handlers[self.LIST]) diff --git a/neutron/pecan_wsgi/hooks/policy_enforcement.py b/neutron/pecan_wsgi/hooks/policy_enforcement.py index 72a9668b039..60e97f8a30f 100644 --- a/neutron/pecan_wsgi/hooks/policy_enforcement.py +++ b/neutron/pecan_wsgi/hooks/policy_enforcement.py @@ -35,7 +35,8 @@ def _custom_getter(resource, resource_id): return quota.get_tenant_quotas(resource_id)[quotasv2.RESOURCE_NAME] -def fetch_resource(neutron_context, collection, resource, resource_id): +def fetch_resource(neutron_context, collection, resource, resource_id, + parent_id=None): controller = manager.NeutronManager.get_controller_for_resource( collection) attrs = controller.resource_info @@ -50,9 +51,11 @@ def fetch_resource(neutron_context, collection, resource, resource_id): value.get('primary_key') or 'default' not in value)] plugin = manager.NeutronManager.get_plugin_for_resource(resource) if plugin: - getter = getattr(plugin, 'get_%s' % resource) - # TODO(kevinbenton): the parent_id logic currently in base.py - return getter(neutron_context, resource_id, fields=field_list) + getter = controller.plugin_shower + getter_args = [neutron_context, resource_id] + if parent_id: + getter_args.append(parent_id) + return getter(*getter_args, fields=field_list) else: # Some legit resources, like quota, do not have a plugin yet. # Retrieving the original object is nevertheless important @@ -81,8 +84,13 @@ class PolicyHook(hooks.PecanHook): needs_prefetch = (state.request.method == 'PUT' or state.request.method == 'DELETE') policy.init() - action = '%s_%s' % (pecan_constants.ACTION_MAP[state.request.method], - resource) + + # NOTE(tonytan4ever): needs to get the actual action from controller's + # _plugin_handlers + controller = manager.NeutronManager.get_controller_for_resource( + collection) + action = controller.plugin_handlers[ + pecan_constants.ACTION_MAP[state.request.method]] # NOTE(salv-orlando): As bulk updates are not supported, in case of PUT # requests there will be only a single item to process, and its @@ -97,8 +105,10 @@ class PolicyHook(hooks.PecanHook): # Ops... this was a delete after all! item = {} resource_id = state.request.context.get('resource_id') + parent_id = state.request.context.get('parent_id') resource_obj = fetch_resource(neutron_context, collection, - resource, resource_id) + resource, resource_id, + parent_id=parent_id) if resource_obj: original_resources.append(resource_obj) obj = copy.copy(resource_obj) diff --git a/neutron/tests/functional/pecan_wsgi/test_controllers.py b/neutron/tests/functional/pecan_wsgi/test_controllers.py index b45c863ddf4..c00ff7a8044 100644 --- a/neutron/tests/functional/pecan_wsgi/test_controllers.py +++ b/neutron/tests/functional/pecan_wsgi/test_controllers.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + import mock from neutron_lib import constants as n_const from oslo_config import cfg @@ -761,7 +763,8 @@ class TestShimControllers(test_functional.PecanFunctionalTest): policy._ENFORCER.set_rules( oslo_policy.Rules.from_dict( {'get_meh_meh': '', - 'get_meh_mehs': ''}), + 'get_meh_mehs': '', + 'get_fake_subresources': ''}), overwrite=False) self.addCleanup(policy.reset) @@ -781,3 +784,19 @@ class TestShimControllers(test_functional.PecanFunctionalTest): resp = self.app.get(url) self.assertEqual(200, resp.status_int) self.assertEqual({body_collection: [{'fake': 'fake'}]}, resp.json) + + def test_hyphenated_collection_subresource_controller_not_shimmed(self): + body_collection = pecan_utils.FakeExtension.HYPHENATED_COLLECTION + uri_collection = body_collection.replace('_', '-') + # there is only one subresource so far + sub_resource_collection = ( + pecan_utils.FakeExtension.FAKE_SUB_RESOURCE_COLLECTION) + temp_id = str(uuid.uuid1()) + url = '/v2.0/{0}/{1}/{2}'.format( + uri_collection, + temp_id, + sub_resource_collection.replace('_', '-')) + resp = self.app.get(url) + self.assertEqual(200, resp.status_int) + self.assertEqual({sub_resource_collection: {'foo': temp_id}}, + resp.json) diff --git a/neutron/tests/functional/pecan_wsgi/utils.py b/neutron/tests/functional/pecan_wsgi/utils.py index d1db7d9ac1c..c9b0e01968f 100644 --- a/neutron/tests/functional/pecan_wsgi/utils.py +++ b/neutron/tests/functional/pecan_wsgi/utils.py @@ -110,6 +110,19 @@ class FakeExtension(extensions.ExtensionDescriptor): HYPHENATED_RESOURCE = 'meh_meh' HYPHENATED_COLLECTION = HYPHENATED_RESOURCE + 's' + SUB_RESOURCE_ATTRIBUTE_MAP = { + 'fake_subresources': { + 'parent': { + 'collection_name': ( + HYPHENATED_COLLECTION), + 'member_name': HYPHENATED_RESOURCE}, + 'parameters': {'foo': {'is_visible': True}, + 'bar': {'is_visible': True} + } + } + } + FAKE_SUB_RESOURCE_COLLECTION = 'fake_subresources' + RAM = { HYPHENATED_COLLECTION: { 'fake': {'is_visible': True} @@ -137,12 +150,34 @@ class FakeExtension(extensions.ExtensionDescriptor): params = self.RAM.get(self.HYPHENATED_COLLECTION, {}) attributes.PLURALS.update({self.HYPHENATED_COLLECTION: self.HYPHENATED_RESOURCE}) + fake_plugin = FakePlugin() controller = base.create_resource( collection, self.HYPHENATED_RESOURCE, FakePlugin(), params, allow_bulk=True, allow_pagination=True, allow_sorting=True) - return [extensions.ResourceExtension(collection, controller, - attr_map=params)] + resources = [extensions.ResourceExtension(collection, + controller, + attr_map=params)] + for collection_name in self.SUB_RESOURCE_ATTRIBUTE_MAP: + resource_name = collection_name + parent = self.SUB_RESOURCE_ATTRIBUTE_MAP[collection_name].get( + 'parent') + params = self.SUB_RESOURCE_ATTRIBUTE_MAP[collection_name].get( + 'parameters') + + controller = base.create_resource(collection_name, resource_name, + fake_plugin, params, + allow_bulk=True, + parent=parent) + + resource = extensions.ResourceExtension( + collection_name, + controller, parent, + path_prefix="", + attr_map=params) + resources.append(resource) + + return resources def get_extended_resources(self, version): if version == "2.0": @@ -165,3 +200,7 @@ class FakePlugin(object): def get_meh_mehs(self, context, filters=None, fields=None): return [{'fake': 'fake'}] + + def get_meh_meh_fake_subresources(self, context, id_, fields=None, + filters=None): + return {'foo': id_}