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
This commit is contained in:
tonytan4ever 2016-06-30 14:29:16 -04:00
parent 97c491294c
commit 9e87a70a1a
5 changed files with 121 additions and 22 deletions

View File

@ -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)}

View File

@ -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])

View File

@ -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)

View File

@ -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)

View File

@ -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_}