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:
parent
97c491294c
commit
9e87a70a1a
|
@ -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)}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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_}
|
||||
|
|
Loading…
Reference in New Issue