diff --git a/neutron/api/v2/base.py b/neutron/api/v2/base.py index f83076f724e..6699d9ab0d9 100644 --- a/neutron/api/v2/base.py +++ b/neutron/api/v2/base.py @@ -71,6 +71,10 @@ class Controller(object): def attr_info(self): return self._attr_info + @property + def member_actions(self): + return self._member_actions + def __init__(self, plugin, collection, resource, attr_info, allow_bulk=False, member_actions=None, parent=None, allow_pagination=False, allow_sorting=False): diff --git a/neutron/extensions/l3.py b/neutron/extensions/l3.py index 50d8aefa6b3..a8d5029bed4 100644 --- a/neutron/extensions/l3.py +++ b/neutron/extensions/l3.py @@ -24,9 +24,6 @@ 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,18 +198,6 @@ class L3(extensions.ExtensionDescriptor): super(L3, self).update_attributes_map( attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) - @classmethod - 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": return RESOURCE_ATTRIBUTE_MAP diff --git a/neutron/pecan_wsgi/controllers/__init__.py b/neutron/pecan_wsgi/controllers/__init__.py index 1f840d77223..f8ea6dd7223 100644 --- a/neutron/pecan_wsgi/controllers/__init__.py +++ b/neutron/pecan_wsgi/controllers/__init__.py @@ -12,9 +12,7 @@ from neutron.pecan_wsgi.controllers import quota from neutron.pecan_wsgi.controllers import resource -from neutron.pecan_wsgi.controllers import router CollectionsController = resource.CollectionsController QuotasController = quota.QuotasController -RoutersController = router.RoutersController diff --git a/neutron/pecan_wsgi/controllers/resource.py b/neutron/pecan_wsgi/controllers/resource.py index dd48333236f..6909fb65950 100644 --- a/neutron/pecan_wsgi/controllers/resource.py +++ b/neutron/pecan_wsgi/controllers/resource.py @@ -27,10 +27,11 @@ LOG = logging.getLogger(__name__) class ItemController(utils.NeutronPecanController): def __init__(self, resource, item, plugin=None, resource_info=None, - parent_resource=None): + parent_resource=None, member_actions=None): super(ItemController, self).__init__(None, resource, plugin=plugin, resource_info=resource_info, - parent_resource=parent_resource) + parent_resource=parent_resource, + member_actions=member_actions) self.item = item @utils.expose(generic=True) @@ -84,11 +85,24 @@ class ItemController(utils.NeutronPecanController): controller = manager.NeutronManager.get_controller_for_resource( collection) if not controller: - LOG.warning(_LW("No controller found for: %s - returning response " - "code 404"), collection) - pecan.abort(404) + if collection not in self._member_actions: + LOG.warning(_LW("No controller found for: %s - returning" + "response code 404"), collection) + pecan.abort(404) + # collection is a member action, so we create a new controller + # for it. + method = self._member_actions[collection] + kwargs = {'plugin': self.plugin, + 'resource_info': self.resource_info} + if method == 'PUT': + kwargs['update_action'] = collection + elif method == 'GET': + kwargs['show_action'] = collection + controller = MemberActionController( + self.resource, self.item, self, **kwargs) + else: + request.context['parent_id'] = request.context['resource_id'] request.context['resource'] = controller.resource - request.context['parent_id'] = request.context['resource_id'] return controller, remainder @@ -104,11 +118,10 @@ class CollectionsController(utils.NeutronPecanController): request.context['uri_identifiers'][uri_identifier] = item return (self.item_controller_class( self.resource, item, resource_info=self.resource_info, - # NOTE(tonytan4ever): item needs to share the same - # parent as collection - parent_resource=self.parent - ), - remainder) + # NOTE(tonytan4ever): item needs to share the same + # parent as collection + parent_resource=self.parent, + member_actions=self._member_actions), remainder) @utils.expose(generic=True) def index(self, *args, **kwargs): @@ -154,3 +167,60 @@ class CollectionsController(utils.NeutronPecanController): creator_args.append(request.context['parent_id']) creator_args.append(data) return {key: creator(*creator_args)} + + +class MemberActionController(ItemController): + @property + def plugin_shower(self): + # NOTE(blogan): Do an explicit check for the _show_action because + # pecan will see the plugin_shower property as a possible custom route + # and try to evaluate it, which causes the code block to be executed. + # If _show_action is None, getattr throws an exception and fails a + # request. + if self._show_action: + return getattr(self.plugin, self._show_action) + + @property + def plugin_updater(self): + if self._update_action: + return getattr(self.plugin, self._update_action) + + def __init__(self, resource, item, parent_controller, plugin=None, + resource_info=None, show_action=None, update_action=None): + super(MemberActionController, self).__init__( + resource, item, plugin=plugin, resource_info=resource_info) + self._show_action = show_action + self._update_action = update_action + self.parent_controller = parent_controller + + @utils.expose(generic=True) + def index(self, *args, **kwargs): + if not self._show_action: + pecan.abort(405) + neutron_context = request.context['neutron_context'] + fields = request.context['query_params'].get('fields') + return self.plugin_shower(neutron_context, self.item, fields=fields) + + @utils.when(index, method='PUT') + def put(self, *args, **kwargs): + if not self._update_action: + LOG.debug("Action %(action)s is not defined on resource " + "%(resource)s", + {'action': self._update_action, + 'resource': self.resource}) + pecan.abort(405) + neutron_context = request.context['neutron_context'] + LOG.debug("Processing member action %(action)s for resource " + "%(resource)s identified by %(item)s", + {'action': self._update_action, + 'resource': self.resource, + 'item': self.item}) + return self.plugin_updater(neutron_context, self.item, + request.context['request_data']) + + @utils.when(index, method='HEAD') + @utils.when(index, method='POST') + @utils.when(index, method='PATCH') + @utils.when(index, method='DELETE') + def not_supported(self): + return super(MemberActionController, self).not_supported() diff --git a/neutron/pecan_wsgi/controllers/router.py b/neutron/pecan_wsgi/controllers/router.py deleted file mode 100644 index 68873de4987..00000000000 --- a/neutron/pecan_wsgi/controllers/router.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) 2015 Taturiello Consulting, Meh. -# 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 neutron._i18n import _LE -from oslo_log import log -import pecan -from pecan import request - -from neutron import manager -from neutron.pecan_wsgi.controllers import resource -from neutron.pecan_wsgi.controllers import utils - -LOG = log.getLogger(__name__) - - -class RouterController(resource.ItemController): - """Customize ResourceController for member actions""" - - ### Pecan generic controllers don't work very well with inheritance - - @utils.expose(generic=True) - def index(self, *args, **kwargs): - return super(RouterController, self).index(*args, **kwargs) - - @utils.when(index, method='HEAD') - @utils.when(index, method='POST') - @utils.when(index, method='PATCH') - def not_supported(self): - return super(RouterController, self).not_supported() - - @utils.when(index, method='PUT') - def put(self, *args, **kwargs): - return super(RouterController, self).put(*args, **kwargs) - - @utils.when(index, method='DELETE') - def delete(self): - return super(RouterController, self).delete() - - @utils.expose() - def _lookup(self, member_action, *remainder): - # This check is mainly for the l3-agents resource. If there isn't - # a controller for it then we'll just assume its a member action. - controller = manager.NeutronManager.get_controller_for_resource( - member_action) - if not controller: - controller = RouterMemberActionController( - self.resource, self.item, member_action) - return controller, remainder - - -class RoutersController(resource.CollectionsController): - - item_controller_class = RouterController - - def __init__(self): - super(RoutersController, self).__init__('routers', 'router') - - -class RouterMemberActionController(resource.ItemController): - - def __init__(self, resource, item, member_action): - super(RouterMemberActionController, self).__init__(resource, item) - self.member_action = member_action - - @utils.expose(generic=True) - def index(self, *args, **kwargs): - pecan.abort(405) - - @utils.when(index, method='HEAD') - @utils.when(index, method='POST') - @utils.when(index, method='PATCH') - def not_supported(self): - return super(RouterMemberActionController, self).not_supported() - - @utils.when(index, method='PUT') - def put(self, *args, **kwargs): - neutron_context = request.context['neutron_context'] - LOG.debug("Processing member action %(action)s for resource " - "%(resource)s identified by %(item)s", - {'action': self.member_action, - 'resource': self.resource, - 'item': self.item}) - # NOTE(salv-orlando): The following simply verify that the plugin - # has a method for a given action. It therefore enables plugins to - # implement actions which are not part of the API specification. - # Unfortunately the API extension descriptor does not do a good job - # of sanctioning which actions are available on a given resource. - # TODO(salv-orlando): prevent plugins from implementing actions - # which are not part of the Neutron API spec - try: - member_action_method = getattr(self.plugin, self.member_action) - return member_action_method(neutron_context, self.item, - request.context['request_data']) - except AttributeError: - LOG.error(_LE("Action %(action)s is not defined on resource " - "%(resource)s"), - {'action': self.member_action, - 'resource': self.resource}) - pecan.abort(404) diff --git a/neutron/pecan_wsgi/controllers/utils.py b/neutron/pecan_wsgi/controllers/utils.py index e195711c53b..de8ced7eb71 100644 --- a/neutron/pecan_wsgi/controllers/utils.py +++ b/neutron/pecan_wsgi/controllers/utils.py @@ -98,10 +98,11 @@ class NeutronPecanController(object): def __init__(self, collection, resource, plugin=None, resource_info=None, allow_pagination=None, allow_sorting=None, - parent_resource=None): + parent_resource=None, member_actions=None): # Ensure dashes are always replaced with underscores self.collection = collection and collection.replace('-', '_') self.resource = resource and resource.replace('-', '_') + self._member_actions = member_actions or {} self._resource_info = resource_info self._plugin = plugin # Controllers for some resources that are not mapped to anything in diff --git a/neutron/pecan_wsgi/hooks/notifier.py b/neutron/pecan_wsgi/hooks/notifier.py index 96b42409478..4277a40261b 100644 --- a/neutron/pecan_wsgi/hooks/notifier.py +++ b/neutron/pecan_wsgi/hooks/notifier.py @@ -23,6 +23,7 @@ from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.common import rpc as n_rpc from neutron import manager from neutron.pecan_wsgi import constants as pecan_constants +from neutron.pecan_wsgi.hooks import utils LOG = log.getLogger(__name__) @@ -66,6 +67,8 @@ class NotifierHook(hooks.PecanHook): resource = state.request.context.get('resource') if not resource: return + if utils.is_member_action(utils.get_controller(state)): + return action = pecan_constants.ACTION_MAP.get(state.request.method) event = '%s.%s.start' % (resource, action) if action in ('create', 'update'): @@ -92,6 +95,8 @@ class NotifierHook(hooks.PecanHook): if not action or action == 'get': LOG.debug("No notification will be sent for action: %s", action) return + if utils.is_member_action(utils.get_controller(state)): + return if state.response.status_int > 300: LOG.debug("No notification will be sent due to unsuccessful " "status code: %s", state.response.status_int) diff --git a/neutron/pecan_wsgi/hooks/policy_enforcement.py b/neutron/pecan_wsgi/hooks/policy_enforcement.py index a0ca98a3118..d9169f1371c 100644 --- a/neutron/pecan_wsgi/hooks/policy_enforcement.py +++ b/neutron/pecan_wsgi/hooks/policy_enforcement.py @@ -26,6 +26,7 @@ from neutron.extensions import quotasv2 from neutron import manager from neutron.pecan_wsgi import constants as pecan_constants from neutron.pecan_wsgi.controllers import quota +from neutron.pecan_wsgi.hooks import utils from neutron import policy @@ -35,10 +36,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, controller, resource, resource_id, parent_id=None): - controller = manager.NeutronManager.get_controller_for_resource( - collection) attrs = controller.resource_info if not attrs: # this isn't a request for a normal resource. it could be @@ -51,7 +50,10 @@ 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 = controller.plugin_shower + if utils.is_member_action(controller): + getter = controller.parent_controller.plugin_shower + else: + getter = controller.plugin_shower getter_args = [neutron_context, resource_id] if parent_id: getter_args.append(parent_id) @@ -80,15 +82,14 @@ class PolicyHook(hooks.PecanHook): # policies if not resource: return + controller = utils.get_controller(state) + if not controller or utils.is_member_action(controller): + return collection = state.request.context.get('collection') needs_prefetch = (state.request.method == 'PUT' or state.request.method == 'DELETE') policy.init() - # 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]] @@ -106,7 +107,7 @@ class PolicyHook(hooks.PecanHook): 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_obj = fetch_resource(neutron_context, controller, resource, resource_id, parent_id=parent_id) if resource_obj: @@ -141,6 +142,7 @@ class PolicyHook(hooks.PecanHook): neutron_context = state.request.context.get('neutron_context') resource = state.request.context.get('resource') collection = state.request.context.get('collection') + controller = utils.get_controller(state) if not resource: # can't filter a resource we don't recognize return @@ -165,8 +167,8 @@ class PolicyHook(hooks.PecanHook): policy_method = policy.enforce if is_single else policy.check plugin = manager.NeutronManager.get_plugin_for_resource(resource) try: - resp = [self._get_filtered_item(state.request, resource, - collection, item) + resp = [self._get_filtered_item(state.request, controller, + resource, collection, item) for item in to_process if (state.request.method != 'GET' or policy_method(neutron_context, action, item, @@ -182,10 +184,11 @@ class PolicyHook(hooks.PecanHook): resp = resp[0] state.response.json = {key: resp} - def _get_filtered_item(self, request, resource, collection, data): + def _get_filtered_item(self, request, controller, resource, collection, + data): neutron_context = request.context.get('neutron_context') to_exclude = self._exclude_attributes_by_policy( - neutron_context, resource, collection, data) + neutron_context, controller, resource, collection, data) return self._filter_attributes(request, data, to_exclude) def _filter_attributes(self, request, data, fields_to_strip): @@ -197,7 +200,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, + def _exclude_attributes_by_policy(self, context, controller, resource, collection, data): """Identifies attributes to exclude according to authZ policies. @@ -207,8 +210,6 @@ class PolicyHook(hooks.PecanHook): """ attributes_to_exclude = [] for attr_name in data.keys(): - controller = manager.NeutronManager.get_controller_for_resource( - collection) attr_data = controller.resource_info.get(attr_name) if attr_data and attr_data['is_visible']: if policy.check( diff --git a/neutron/pecan_wsgi/hooks/query_parameters.py b/neutron/pecan_wsgi/hooks/query_parameters.py index 82c9316cbfd..6cf63cc7096 100644 --- a/neutron/pecan_wsgi/hooks/query_parameters.py +++ b/neutron/pecan_wsgi/hooks/query_parameters.py @@ -15,6 +15,7 @@ from pecan import hooks from neutron.api import api_common from neutron import manager from neutron.pecan_wsgi.hooks import policy_enforcement +from neutron.pecan_wsgi.hooks import utils # TODO(blogan): ideally it'd be nice to get the pagination and sorting @@ -93,8 +94,7 @@ class QueryParametersHook(hooks.PecanHook): collection = state.request.context.get('collection') if not collection: return - controller = manager.NeutronManager.get_controller_for_resource( - collection) + controller = utils.get_controller(state) combined_fields, added_fields = _set_fields(state, controller) filters = _set_filters(state, controller) query_params = {'fields': combined_fields, 'filters': filters} diff --git a/neutron/pecan_wsgi/hooks/utils.py b/neutron/pecan_wsgi/hooks/utils.py new file mode 100644 index 00000000000..396aea0242b --- /dev/null +++ b/neutron/pecan_wsgi/hooks/utils.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015 Taturiello Consulting, Meh. +# 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 neutron.pecan_wsgi.controllers import resource +from neutron.pecan_wsgi.controllers import utils as controller_utils + + +def get_controller(state): + if (state.arguments and state.arguments.args and + isinstance(state.arguments.args[0], + controller_utils.NeutronPecanController)): + controller = state.arguments.args[0] + return controller + + +def is_member_action(controller): + return isinstance(controller, + resource.MemberActionController) diff --git a/neutron/pecan_wsgi/startup.py b/neutron/pecan_wsgi/startup.py index a98cda37276..34287419f4f 100644 --- a/neutron/pecan_wsgi/startup.py +++ b/neutron/pecan_wsgi/startup.py @@ -70,6 +70,7 @@ def initialize_all(): resource = legacy_controller.resource plugin = legacy_controller.plugin attr_info = legacy_controller.attr_info + member_actions = legacy_controller.member_actions # Retrieving the parent resource. It is expected the format of # the parent resource to be: # {'collection_name': 'name-of-collection', @@ -80,7 +81,7 @@ def initialize_all(): parent_resource = parent.get('member_name') new_controller = res_ctrl.CollectionsController( collection, resource, resource_info=attr_info, - parent_resource=parent_resource) + parent_resource=parent_resource, member_actions=member_actions) manager.NeutronManager.set_plugin_for_resource(resource, plugin) if path_prefix: manager.NeutronManager.add_resource_for_path_prefix( diff --git a/neutron/tests/functional/pecan_wsgi/test_controllers.py b/neutron/tests/functional/pecan_wsgi/test_controllers.py index 4a8a52f6e74..748e45f7132 100644 --- a/neutron/tests/functional/pecan_wsgi/test_controllers.py +++ b/neutron/tests/functional/pecan_wsgi/test_controllers.py @@ -802,3 +802,51 @@ class TestShimControllers(test_functional.PecanFunctionalTest): self.assertEqual(200, resp.status_int) self.assertEqual({sub_resource_collection: {'foo': temp_id}}, resp.json) + + +class TestMemberActionController(test_functional.PecanFunctionalTest): + def setUp(self): + fake_ext = pecan_utils.FakeExtension() + fake_plugin = pecan_utils.FakePlugin() + plugins = {pecan_utils.FakePlugin.PLUGIN_TYPE: fake_plugin} + new_extensions = {fake_ext.get_alias(): fake_ext} + super(TestMemberActionController, self).setUp( + service_plugins=plugins, extensions=new_extensions) + hyphen_collection = pecan_utils.FakeExtension.HYPHENATED_COLLECTION + self.collection = hyphen_collection.replace('_', '-') + + def test_get_member_action_controller(self): + url = '/v2.0/{}/something/boo_meh.json'.format(self.collection) + resp = self.app.get(url) + self.assertEqual(200, resp.status_int) + self.assertEqual({'boo_yah': 'something'}, resp.json) + + def test_put_member_action_controller(self): + url = '/v2.0/{}/something/put_meh.json'.format(self.collection) + resp = self.app.put_json(url, params={'it_matters_not': 'ok'}) + self.assertEqual(200, resp.status_int) + self.assertEqual({'poo_yah': 'something'}, resp.json) + + def test_get_member_action_does_not_exist(self): + url = '/v2.0/{}/something/are_you_still_there.json'.format( + self.collection) + resp = self.app.get(url, expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_put_member_action_does_not_exist(self): + url = '/v2.0/{}/something/are_you_still_there.json'.format( + self.collection) + resp = self.app.put_json(url, params={'it_matters_not': 'ok'}, + expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_put_on_get_member_action(self): + url = '/v2.0/{}/something/boo_meh.json'.format(self.collection) + resp = self.app.put_json(url, params={'it_matters_not': 'ok'}, + expect_errors=True) + self.assertEqual(405, resp.status_int) + + def test_get_on_put_member_action(self): + url = '/v2.0/{}/something/put_meh.json'.format(self.collection) + resp = self.app.get(url, expect_errors=True) + self.assertEqual(405, resp.status_int) diff --git a/neutron/tests/functional/pecan_wsgi/test_hooks.py b/neutron/tests/functional/pecan_wsgi/test_hooks.py index 10499ff39b2..eea01b5796c 100644 --- a/neutron/tests/functional/pecan_wsgi/test_hooks.py +++ b/neutron/tests/functional/pecan_wsgi/test_hooks.py @@ -335,7 +335,9 @@ class TestNovaNotifierHook(test_functional.PecanFunctionalTest): # NOTE(kevinbenton): the original passed into the notifier does # not contain all of the fields of the object. Only those required # by the policy engine are included. - orig = pe.fetch_resource(context.get_admin_context(), 'networks', + controller = manager.NeutronManager.get_controller_for_resource( + 'networks') + orig = pe.fetch_resource(context.get_admin_context(), controller, 'network', network_id) response = self.app.put_json( '/v2.0/networks/%s.json' % network_id, @@ -347,7 +349,7 @@ class TestNovaNotifierHook(test_functional.PecanFunctionalTest): orig, json_body) self.mock_notifier.reset_mock() - orig = pe.fetch_resource(context.get_admin_context(), 'networks', + orig = pe.fetch_resource(context.get_admin_context(), controller, 'network', network_id) response = self.app.delete( '/v2.0/networks/%s.json' % network_id, headers=req_headers) diff --git a/neutron/tests/functional/pecan_wsgi/utils.py b/neutron/tests/functional/pecan_wsgi/utils.py index c9b0e01968f..8bd0c240712 100644 --- a/neutron/tests/functional/pecan_wsgi/utils.py +++ b/neutron/tests/functional/pecan_wsgi/utils.py @@ -150,14 +150,15 @@ class FakeExtension(extensions.ExtensionDescriptor): params = self.RAM.get(self.HYPHENATED_COLLECTION, {}) attributes.PLURALS.update({self.HYPHENATED_COLLECTION: self.HYPHENATED_RESOURCE}) + member_actions = {'put_meh': 'PUT', 'boo_meh': 'GET'} fake_plugin = FakePlugin() controller = base.create_resource( collection, self.HYPHENATED_RESOURCE, FakePlugin(), params, allow_bulk=True, allow_pagination=True, - allow_sorting=True) - resources = [extensions.ResourceExtension(collection, - controller, - attr_map=params)] + allow_sorting=True, member_actions=member_actions) + resources = [extensions.ResourceExtension( + collection, controller, attr_map=params, + member_actions=member_actions)] for collection_name in self.SUB_RESOURCE_ATTRIBUTE_MAP: resource_name = collection_name parent = self.SUB_RESOURCE_ATTRIBUTE_MAP[collection_name].get( @@ -204,3 +205,9 @@ class FakePlugin(object): def get_meh_meh_fake_subresources(self, context, id_, fields=None, filters=None): return {'foo': id_} + + def put_meh(self, context, id_, data): + return {'poo_yah': id_} + + def boo_meh(self, context, id_, fields=None): + return {'boo_yah': id_}