Merge "Pecan: Handle member actions"

This commit is contained in:
Jenkins 2016-08-30 13:00:38 +00:00 committed by Gerrit Code Review
commit 8eaa1808e5
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

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

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