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:
parent
d51a18c4d0
commit
ea4ba642dd
@ -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(
|
||||
|
18
neutron/pecan_wsgi/constants.py
Normal file
18
neutron/pecan_wsgi/constants.py
Normal 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'}
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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')])
|
||||
|
Loading…
Reference in New Issue
Block a user