Pecan: implement DHCP notifications in NotifierHook

This patch adds support for DHCP notifications into the Notifier hook,
which so far has been pretty much a placeholder for future work.

To this aim its priority has been changed in order to ensure the
'after' hook is executed after the 'after' hook for the policy engine.
This will ensure that any 'alien' data returned from the plugin is
stripped off before being sent to the notifiers, as well as any
attribute that the user is not authorized to access.

Since delete operations require to send the original object to the
notifier, this patch leverages the "prefetch" feature of the policy
hook to avoid loading again the object from the plugin. This is not
ideal and will be fixed in another patch where prefetching will be
performed in its own hook.

The ACTION_MAP constant has been factored out in an appropriate
module for constants as it is now used by the NotifierHook class as
well. The decision of using a new constant module is rather
arbitrary as the module neutron.common.constants could have been
used as well.

With this patch, the notifier hook only sends events signalling
completion of operations (e.g.: network.create.end) as these are
the only events processed by the DHCP agent. Support for 'start'
events will be added in a subsequent patch.

Related-Blueprint: pecan-wsgi-switch

Change-Id: I69680952f99c404d4535db48db73fc815977f2ee
This commit is contained in:
Salvatore Orlando 2016-02-09 15:24:39 -08:00
parent d51a18c4d0
commit ea4ba642dd
5 changed files with 168 additions and 15 deletions

View File

@ -49,8 +49,8 @@ def setup_app(*args, **kwargs):
hooks.BodyValidationHook(), # priority 120
hooks.OwnershipValidationHook(), # priority 125
hooks.QuotaEnforcementHook(), # priority 130
hooks.PolicyHook(), # priority 135
hooks.NotifierHook(), # priority 140
hooks.NotifierHook(), # priority 135
hooks.PolicyHook(), # priority 140
]
app = pecan.make_app(

View File

@ -0,0 +1,18 @@
# 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.
ACTION_MAP = {'POST': 'create',
'PUT': 'update',
'GET': 'get',
'DELETE': 'delete'}

View File

@ -13,18 +13,76 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_log import log
from oslo_serialization import jsonutils
from pecan import hooks
from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api
from neutron.common import constants
from neutron import manager
from neutron.pecan_wsgi import constants as pecan_constants
LOG = log.getLogger(__name__)
class NotifierHook(hooks.PecanHook):
priority = 140
priority = 135
# TODO(kevinbenton): implement
# dhcp agent notifier
# ceilo notifier
# nova notifier
def before(self, state):
pass
def _notify_dhcp_agent(self, context, resource_name, action, resources):
plugin = manager.NeutronManager.get_plugin_for_resource(resource_name)
notifier_method = '%s.%s.end' % (resource_name, action)
# use plugin's dhcp notifier, if this is already instantiated
agent_notifiers = getattr(plugin, 'agent_notifiers', {})
dhcp_agent_notifier = (
agent_notifiers.get(constants.AGENT_TYPE_DHCP) or
dhcp_rpc_agent_api.DhcpAgentNotifyAPI()
)
# The DHCP Agent does not accept bulk notifications
for resource in resources:
item = {resource_name: resource}
LOG.debug("Sending DHCP agent notification for: %s", item)
dhcp_agent_notifier.notify(context, item, notifier_method)
def after(self, state):
pass
# if the after hook is executed the request completed successfully and
# therefore notifications must be sent
resource_name = state.request.context.get('resource')
collection_name = state.request.context.get('collection')
neutron_context = state.request.context.get('neutron_context')
if not resource_name:
LOG.debug("Skipping NotifierHook processing as there was no "
"resource associated with the request")
return
action = pecan_constants.ACTION_MAP.get(state.request.method)
if not action or action == 'get':
LOG.debug("No notification will be sent for action: %s", action)
return
if action == 'delete':
# The object has been deleted, so we must notify the agent with the
# data of the original object
data = {collection_name:
state.request.context.get('request_resources', [])}
else:
try:
data = jsonutils.loads(state.response.body)
except ValueError:
if not state.response.body:
data = {}
if cfg.CONF.dhcp_agent_notification:
if data:
if resource_name in data:
resources = [data[resource_name]]
elif collection_name in data:
# This was a bulk request
resources = data[collection_name]
else:
resources = []
self._notify_dhcp_agent(neutron_context, resource_name,
action, resources)

View File

@ -26,6 +26,7 @@ from neutron.api.v2 import attributes as v2_attributes
from neutron.common import constants as const
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 import policy
@ -37,9 +38,7 @@ def _custom_getter(resource, resource_id):
class PolicyHook(hooks.PecanHook):
priority = 135
ACTION_MAP = {'POST': 'create', 'PUT': 'update', 'GET': 'get',
'DELETE': 'delete'}
priority = 140
def _fetch_resource(self, neutron_context, resource, resource_id):
attrs = v2_attributes.get_resource_info(resource)
@ -75,13 +74,15 @@ class PolicyHook(hooks.PecanHook):
needs_prefetch = (state.request.method == 'PUT' or
state.request.method == 'DELETE')
policy.init()
action = '%s_%s' % (self.ACTION_MAP[state.request.method], resource)
action = '%s_%s' % (pecan_constants.ACTION_MAP[state.request.method],
resource)
# 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
# identifier would have been already retrieved by the lookup process;
# in the case of DELETE requests there won't be any item to process in
# the request body
merged_resources = []
if needs_prefetch:
try:
item = resources_copy.pop()
@ -93,10 +94,14 @@ class PolicyHook(hooks.PecanHook):
resource,
resource_id))
obj.update(item)
merged_resources.append(obj.copy())
obj[const.ATTRIBUTES_TO_UPDATE] = item.keys()
# Put back the item in the list so that policies could be enforced
resources_copy.append(obj)
# TODO(salv-orlando): as other hooks might need to prefetch resources,
# store them in the request context. However, this should be done in a
# separate hook which is conventietly called before all other hooks
state.request.context['request_resources'] = merged_resources
for item in resources_copy:
try:
policy.enforce(
@ -127,7 +132,7 @@ class PolicyHook(hooks.PecanHook):
data = state.response.json
except simplejson.JSONDecodeError:
return
action = '%s_%s' % (self.ACTION_MAP[state.request.method],
action = '%s_%s' % (pecan_constants.ACTION_MAP[state.request.method],
resource)
if not data or (resource not in data and collection not in data):
return
@ -154,8 +159,7 @@ class PolicyHook(hooks.PecanHook):
if is_single:
resp = resp[0]
data[key] = resp
state.response.json = data
state.response.json = {key: resp}
def _get_filtered_item(self, request, resource, collection, data):
neutron_context = request.context.get('neutron_context')

View File

@ -14,6 +14,7 @@
# under the License.
import mock
from oslo_config import cfg
from oslo_policy import policy as oslo_policy
from oslo_serialization import jsonutils
@ -176,3 +177,75 @@ class TestPolicyEnforcementHook(test_functional.PecanFunctionalTest):
self.assertEqual(200, response.status_int)
json_response = jsonutils.loads(response.body)
self.assertNotIn('restricted_attr', json_response['mehs'][0])
class TestNotifierHook(test_functional.PecanFunctionalTest):
def setUp(self):
# the DHCP notifier needs to be mocked so that correct operations can
# be easily validated. For the purpose of this test it is indeed not
# necessary that the notification is actually received and processed by
# the agent
patcher = mock.patch('neutron.api.rpc.agentnotifiers.'
'dhcp_rpc_agent_api.DhcpAgentNotifyAPI.notify')
self.mock_notifier = patcher.start()
super(TestNotifierHook, self).setUp()
def test_dhcp_notifications_disabled(self):
cfg.CONF.set_override('dhcp_agent_notification', False)
self.app.post_json(
'/v2.0/networks.json',
params={'network': {'name': 'meh'}},
headers={'X-Project-Id': 'tenid'})
self.assertEqual(0, self.mock_notifier.call_count)
def test_get_does_not_trigger_notification(self):
self.do_request('/v2.0/networks', tenant_id='tenid')
self.assertEqual(0, self.mock_notifier.call_count)
def test_post_put_delete_triggers_notification(self):
req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'}
response = self.app.post_json(
'/v2.0/networks.json',
params={'network': {'name': 'meh'}}, headers=req_headers)
self.assertEqual(201, response.status_int)
json_body = jsonutils.loads(response.body)
self.assertEqual(1, self.mock_notifier.call_count)
self.assertEqual(mock.call(mock.ANY, json_body, 'network.create.end'),
self.mock_notifier.mock_calls[-1])
network_id = json_body['network']['id']
response = self.app.put_json(
'/v2.0/networks/%s.json' % network_id,
params={'network': {'name': 'meh-2'}},
headers=req_headers)
self.assertEqual(200, response.status_int)
json_body = jsonutils.loads(response.body)
self.assertEqual(2, self.mock_notifier.call_count)
self.assertEqual(mock.call(mock.ANY, json_body, 'network.update.end'),
self.mock_notifier.mock_calls[-1])
response = self.app.delete(
'/v2.0/networks/%s.json' % network_id, headers=req_headers)
self.assertEqual(204, response.status_int)
self.assertEqual(3, self.mock_notifier.call_count)
# No need to validate data content sent to the notifier as it's just
# going to load the object from the database
self.assertEqual(mock.call(mock.ANY, mock.ANY, 'network.delete.end'),
self.mock_notifier.mock_calls[-1])
def test_bulk_create_triggers_notifications(self):
req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'}
response = self.app.post_json(
'/v2.0/networks.json',
params={'networks': [{'name': 'meh_1'},
{'name': 'meh_2'}]},
headers=req_headers)
self.assertEqual(201, response.status_int)
json_body = jsonutils.loads(response.body)
item_1 = json_body['networks'][0]
item_2 = json_body['networks'][1]
self.assertEqual(2, self.mock_notifier.call_count)
self.mock_notifier.assert_has_calls(
[mock.call(mock.ANY, {'network': item_1}, 'network.create.end'),
mock.call(mock.ANY, {'network': item_2}, 'network.create.end')])