Add metrics notifier to Pecan

This adds the standard 'object.(create|update|delete).(start|end)'
notifications to the Pecan notification hook and adds unit tests
to exercise them.

This patch also corrects the on_error handler for untranslated
exceptions which was incorrectly raising the exception rather than
returning it. This was resulting in the other hooks not getting
the correct status code on an untranslated exception.

Closes-Bug: #1552979
Change-Id: I400f8d3988db204caed25e7c848a415b45d47172
This commit is contained in:
Kevin Benton 2016-02-17 02:41:39 -08:00
parent 718434bf9b
commit e433c2870a
3 changed files with 167 additions and 2 deletions

View File

@ -20,6 +20,7 @@ from pecan import hooks
from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api
from neutron.common import constants
from neutron.common import rpc as n_rpc
from neutron import manager
from neutron.pecan_wsgi import constants as pecan_constants
@ -29,7 +30,11 @@ LOG = log.getLogger(__name__)
class NotifierHook(hooks.PecanHook):
priority = 135
# TODO(kevinbenton): implement ceilo notifier
@property
def _notifier(self):
if not hasattr(self, '_notifier_inst'):
self._notifier_inst = n_rpc.get_notifier('network')
return self._notifier_inst
def _nova_notify(self, action, resource, *args):
action_resource = '%s_%s' % (action, resource)
@ -55,6 +60,26 @@ class NotifierHook(hooks.PecanHook):
LOG.debug("Sending DHCP agent notification for: %s", item)
dhcp_agent_notifier.notify(context, item, notifier_method)
def before(self, state):
if state.request.method not in ('POST', 'PUT', 'DELETE'):
return
resource = state.request.context.get('resource')
if not resource:
return
action = pecan_constants.ACTION_MAP.get(state.request.method)
event = '%s.%s.start' % (resource, action)
if action in ('create', 'update'):
# notifier just gets plain old body without any treatment other
# than the population of the object ID being operated on
payload = state.request.json.copy()
if action == 'update':
payload['id'] = state.request.context.get('resource_id')
elif action == 'delete':
resource_id = state.request.context.get('resource_id')
payload = {resource + '_id': resource_id}
self._notifier.info(state.request.context.get('neutron_context'),
event, payload)
def after(self, state):
# if the after hook is executed the request completed successfully and
# therefore notifications must be sent
@ -109,3 +134,24 @@ class NotifierHook(hooks.PecanHook):
for resource in resources:
self._nova_notify(action, resource_name, orig,
{resource_name: resource})
event = '%s.%s.end' % (resource_name, action)
if action == 'delete':
if state.response.status_int > 300:
# don't notify when unsuccessful
# NOTE(kevinbenton): we may want to be more strict with the
# response codes
return
resource_id = state.request.context.get('resource_id')
payload = {resource_name + '_id': resource_id}
elif action in ('create', 'update'):
if not resources:
# create/update did not complete so no notification
return
if len(resources) > 1:
payload = {collection_name: resources}
else:
payload = {resource_name: resources[0]}
else:
return
self._notifier.info(neutron_context, event, payload)

View File

@ -35,5 +35,5 @@ class ExceptionTranslationHook(hooks.PecanHook):
# leaked unexpected exception, convert to boring old 500 error and
# hide message from user in case it contained sensitive details
LOG.exception(_LE("An unexpected exception was caught: %s"), e)
raise webob.exc.HTTPInternalServerError(
return webob.exc.HTTPInternalServerError(
_("An unexpected internal error occurred."))

View File

@ -361,3 +361,122 @@ class TestNovaNotifierHook(test_functional.PecanFunctionalTest):
[mock.call('create', 'network', {}, {'network': item_1}),
mock.call('create', 'network', {}, {'network': item_2})],
self.mock_notifier.mock_calls)
class TestMetricsNotifierHook(test_functional.PecanFunctionalTest):
def setUp(self):
patcher = mock.patch('neutron.pecan_wsgi.hooks.notifier.NotifierHook.'
'_notifier')
self.mock_notifier = patcher.start().info
super(TestMetricsNotifierHook, self).setUp()
def test_post_put_delete_triggers_notification(self):
req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'}
payload = {'network': {'name': 'meh'}}
response = self.app.post_json(
'/v2.0/networks.json',
params=payload, headers=req_headers)
self.assertEqual(201, response.status_int)
json_body = jsonutils.loads(response.body)
self.assertEqual(
[mock.call(mock.ANY, 'network.create.start', payload),
mock.call(mock.ANY, 'network.create.end', json_body)],
self.mock_notifier.mock_calls)
self.mock_notifier.reset_mock()
network_id = json_body['network']['id']
payload = {'network': {'name': 'meh-2'}}
response = self.app.put_json(
'/v2.0/networks/%s.json' % network_id,
params=payload, headers=req_headers)
self.assertEqual(200, response.status_int)
json_body = jsonutils.loads(response.body)
# id should be in payload sent to notifier
payload['id'] = network_id
self.assertEqual(
[mock.call(mock.ANY, 'network.update.start', payload),
mock.call(mock.ANY, 'network.update.end', json_body)],
self.mock_notifier.mock_calls)
self.mock_notifier.reset_mock()
response = self.app.delete(
'/v2.0/networks/%s.json' % network_id, headers=req_headers)
self.assertEqual(204, response.status_int)
payload = {'network_id': network_id}
self.assertEqual(
[mock.call(mock.ANY, 'network.delete.start', payload),
mock.call(mock.ANY, 'network.delete.end', payload)],
self.mock_notifier.mock_calls)
def test_bulk_create_triggers_notification(self):
req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'}
payload = {'networks': [{'name': 'meh_1'}, {'name': 'meh_2'}]}
response = self.app.post_json(
'/v2.0/networks.json',
params=payload,
headers=req_headers)
self.assertEqual(201, response.status_int)
json_body = jsonutils.loads(response.body)
self.assertEqual(2, self.mock_notifier.call_count)
self.mock_notifier.assert_has_calls(
[mock.call(mock.ANY, 'network.create.start', payload),
mock.call(mock.ANY, 'network.create.end', json_body)])
def test_bad_create_doesnt_emit_end(self):
req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'}
payload = {'network': {'name': 'meh'}}
plugin = manager.NeutronManager.get_plugin()
with mock.patch.object(plugin, 'create_network',
side_effect=ValueError):
response = self.app.post_json(
'/v2.0/networks.json',
params=payload, headers=req_headers,
expect_errors=True)
self.assertEqual(500, response.status_int)
self.assertEqual(
[mock.call(mock.ANY, 'network.create.start', mock.ANY)],
self.mock_notifier.mock_calls)
def test_bad_update_doesnt_emit_end(self):
req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'}
payload = {'network': {'name': 'meh'}}
response = self.app.post_json(
'/v2.0/networks.json',
params=payload, headers=req_headers,
expect_errors=True)
self.assertEqual(201, response.status_int)
json_body = jsonutils.loads(response.body)
self.mock_notifier.reset_mock()
plugin = manager.NeutronManager.get_plugin()
with mock.patch.object(plugin, 'update_network',
side_effect=ValueError):
response = self.app.put_json(
'/v2.0/networks/%s.json' % json_body['network']['id'],
params=payload, headers=req_headers,
expect_errors=True)
self.assertEqual(500, response.status_int)
self.assertEqual(
[mock.call(mock.ANY, 'network.update.start', mock.ANY)],
self.mock_notifier.mock_calls)
def test_bad_delete_doesnt_emit_end(self):
req_headers = {'X-Project-Id': 'tenid', 'X-Roles': 'admin'}
payload = {'network': {'name': 'meh'}}
response = self.app.post_json(
'/v2.0/networks.json',
params=payload, headers=req_headers,
expect_errors=True)
self.assertEqual(201, response.status_int)
json_body = jsonutils.loads(response.body)
self.mock_notifier.reset_mock()
plugin = manager.NeutronManager.get_plugin()
with mock.patch.object(plugin, 'delete_network',
side_effect=ValueError):
response = self.app.delete(
'/v2.0/networks/%s.json' % json_body['network']['id'],
headers=req_headers, expect_errors=True)
self.assertEqual(500, response.status_int)
self.assertEqual(
[mock.call(mock.ANY, 'network.delete.start', mock.ANY)],
self.mock_notifier.mock_calls)