Pecan: Handle member actions

The legacy pecan uri routing allowed the definition of member actions.
These are API resources that hang off a particular resource_id.  An
example of this is:

PUT /routers/{router_id}/add_router_interface

The legacy seemed to do only PUTs and GETs so that has been implemented
in Pecan.  Other methods can easily be added if needed.

Also, with the generic handling of this came the decision to remove the
specific pecan routers controller because it's only function was to
facilitate its member actions to work.  It is no longer needed.

Change-Id: If776476545edca0c4c43ce3969bb5d1af79f4382
This commit is contained in:
Brandon Logan 2016-07-20 01:46:49 -05:00
parent 9e87a70a1a
commit 6e908dd417
14 changed files with 206 additions and 165 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -800,3 +800,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)

View File

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

View File

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