Skip to content

Commit 590d40c

Browse files
author
Balazs Gibizer
committed
Forbid new legacy notification event_type
As we agreed on the Mitaka midcycle no new legacy notification event_type are allowed in Nova and every new notification shall use the versioned notification framework. This patch adds a validation step into the legacy notification sending call chain. It checks the event_type against a whitelist. During unit and functional testing an exception is raised if the event_type is not known and therefore forbiden. This check is not 100% foolproof as it only detects the new event_type if there is a test that triggers the sending of it and this check is not capable of detecting such event_types if the test directly mocks nova.rpc.get_notifier. During normal operation a WARNING is logged so that a logstash query can be set up to detect the rest of the cases. Change-Id: Ia6600868aa7b3a22cb8c020503f49dce6a4c2c2b
1 parent 38c3f88 commit 590d40c

File tree

4 files changed

+195
-3
lines changed

4 files changed

+195
-3
lines changed

nova/rpc.py

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
'TRANSPORT_ALIASES',
2727
]
2828

29+
import functools
30+
31+
from nova.i18n import _
2932
from oslo_config import cfg
33+
from oslo_log import log as logging
3034
import oslo_messaging as messaging
3135
from oslo_serialization import jsonutils
3236

@@ -45,6 +49,8 @@
4549

4650
CONF.register_opts(notification_opts)
4751

52+
LOG = logging.getLogger(__name__)
53+
4854
TRANSPORT = None
4955
LEGACY_NOTIFIER = None
5056
NOTIFICATION_TRANSPORT = None
@@ -179,9 +185,168 @@ def get_notifier(service, host=None, publisher_id=None):
179185
assert LEGACY_NOTIFIER is not None
180186
if not publisher_id:
181187
publisher_id = "%s.%s" % (service, host or CONF.host)
182-
return LEGACY_NOTIFIER.prepare(publisher_id=publisher_id)
188+
return LegacyValidatingNotifier(
189+
LEGACY_NOTIFIER.prepare(publisher_id=publisher_id))
183190

184191

185192
def get_versioned_notifier(publisher_id):
186193
assert NOTIFIER is not None
187194
return NOTIFIER.prepare(publisher_id=publisher_id)
195+
196+
197+
class LegacyValidatingNotifier(object):
198+
"""Wraps an oslo.messaging Notifier and checks for allowed event_types."""
199+
200+
# If true an exception is thrown if the event_type is not allowed, if false
201+
# then only a WARNING is logged
202+
fatal = False
203+
204+
# This list contains the already existing therefore allowed legacy
205+
# notification event_types. New items shall not be added to the list as
206+
# Nova does not allow new legacy notifications any more. This list will be
207+
# removed when all the notification is transformed to versioned
208+
# notifications.
209+
allowed_legacy_notification_event_types = [
210+
'aggregate.addhost.end',
211+
'aggregate.addhost.start',
212+
'aggregate.create.end',
213+
'aggregate.create.start',
214+
'aggregate.delete.end',
215+
'aggregate.delete.start',
216+
'aggregate.removehost.end',
217+
'aggregate.removehost.start',
218+
'aggregate.updatemetadata.end',
219+
'aggregate.updatemetadata.start',
220+
'aggregate.updateprop.end',
221+
'aggregate.updateprop.start',
222+
'api.fault',
223+
'compute.instance.create.end',
224+
'compute.instance.create.error',
225+
'compute.instance.create_ip.end',
226+
'compute.instance.create_ip.start',
227+
'compute.instance.create.start',
228+
'compute.instance.delete.end',
229+
'compute.instance.delete_ip.end',
230+
'compute.instance.delete_ip.start',
231+
'compute.instance.delete.start',
232+
'compute.instance.evacuate',
233+
'compute.instance.exists',
234+
'compute.instance.finish_resize.end',
235+
'compute.instance.finish_resize.start',
236+
'compute.instance.live.migration.abort.start',
237+
'compute.instance.live.migration.abort.end',
238+
'compute.instance.live_migration.post.dest.end',
239+
'compute.instance.live_migration.post.dest.start',
240+
'compute.instance.live_migration._post.end',
241+
'compute.instance.live_migration._post.start',
242+
'compute.instance.live_migration.pre.end',
243+
'compute.instance.live_migration.pre.start',
244+
'compute.instance.live_migration.rollback.dest.end',
245+
'compute.instance.live_migration.rollback.dest.start',
246+
'compute.instance.live_migration._rollback.end',
247+
'compute.instance.live_migration._rollback.start',
248+
'compute.instance.pause.end',
249+
'compute.instance.pause.start',
250+
'compute.instance.power_off.end',
251+
'compute.instance.power_off.start',
252+
'compute.instance.power_on.end',
253+
'compute.instance.power_on.start',
254+
'compute.instance.reboot.end',
255+
'compute.instance.reboot.start',
256+
'compute.instance.rebuild.end',
257+
'compute.instance.rebuild.error',
258+
'compute.instance.rebuild.scheduled',
259+
'compute.instance.rebuild.start',
260+
'compute.instance.rescue.end',
261+
'compute.instance.rescue.start',
262+
'compute.instance.resize.confirm.end',
263+
'compute.instance.resize.confirm.start',
264+
'compute.instance.resize.end',
265+
'compute.instance.resize.error',
266+
'compute.instance.resize.prep.end',
267+
'compute.instance.resize.prep.start',
268+
'compute.instance.resize.revert.end',
269+
'compute.instance.resize.revert.start',
270+
'compute.instance.resize.start',
271+
'compute.instance.restore.end',
272+
'compute.instance.restore.start',
273+
'compute.instance.resume.end',
274+
'compute.instance.resume.start',
275+
'compute.instance.shelve.end',
276+
'compute.instance.shelve_offload.end',
277+
'compute.instance.shelve_offload.start',
278+
'compute.instance.shelve.start',
279+
'compute.instance.shutdown.end',
280+
'compute.instance.shutdown.start',
281+
'compute.instance.snapshot.end',
282+
'compute.instance.snapshot.start',
283+
'compute.instance.soft_delete.end',
284+
'compute.instance.soft_delete.start',
285+
'compute.instance.suspend.end',
286+
'compute.instance.suspend.start',
287+
'compute.instance.trigger_crash_dump.end',
288+
'compute.instance.trigger_crash_dump.start',
289+
'compute.instance.unpause.end',
290+
'compute.instance.unpause.start',
291+
'compute.instance.unrescue.end',
292+
'compute.instance.unrescue.start',
293+
'compute.instance.unshelve.start',
294+
'compute.instance.unshelve.end',
295+
'compute.instance.update',
296+
'compute.instance.volume.attach',
297+
'compute.instance.volume.detach',
298+
'compute.libvirt.error',
299+
'compute_task.build_instances',
300+
'compute_task.migrate_server',
301+
'compute_task.rebuild_server',
302+
'HostAPI.power_action.end',
303+
'HostAPI.power_action.start',
304+
'HostAPI.set_enabled.end',
305+
'HostAPI.set_enabled.start',
306+
'HostAPI.set_maintenance.end',
307+
'HostAPI.set_maintenance.start',
308+
'keypair.create.start',
309+
'keypair.create.end',
310+
'keypair.delete.start',
311+
'keypair.delete.end',
312+
'keypair.import.start',
313+
'keypair.import.end',
314+
'network.floating_ip.allocate',
315+
'network.floating_ip.associate',
316+
'network.floating_ip.deallocate',
317+
'network.floating_ip.disassociate',
318+
'scheduler.select_destinations.end',
319+
'scheduler.select_destinations.start',
320+
'servergroup.addmember',
321+
'servergroup.create',
322+
'servergroup.delete',
323+
'volume.usage',
324+
]
325+
326+
message = _('%(event_type)s is not a versioned notification and not '
327+
'whitelisted. See ./doc/source/notification.rst')
328+
329+
def __init__(self, notifier):
330+
self.notifier = notifier
331+
for priority in ['debug', 'info', 'warn', 'error', 'critical']:
332+
setattr(self, priority,
333+
functools.partial(self._notify, priority))
334+
335+
def _is_wrap_exception_notification(self, payload):
336+
# nova.exception.wrap_exception decorator emits notification where the
337+
# event_type is the name of the decorated function. This is used in
338+
# many places but it will be converted to versioned notification in one
339+
# run by updating the decorator so it is pointless to white list all
340+
# the function names here we white list the notification itself
341+
# detected by the special payload keys.
342+
return {'exception', 'args'} == set(payload.keys())
343+
344+
def _notify(self, priority, ctxt, event_type, payload):
345+
if (event_type not in self.allowed_legacy_notification_event_types and
346+
not self._is_wrap_exception_notification(payload)):
347+
if self.fatal:
348+
raise AssertionError(self.message % {'event_type': event_type})
349+
else:
350+
LOG.warning(self.message, {'event_type': event_type})
351+
352+
getattr(self.notifier, priority)(ctxt, event_type, payload)

nova/test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ def setUp(self):
244244

245245
openstack_driver.DRIVER_CACHE = {}
246246

247+
self.useFixture(nova_fixtures.ForbidNewLegacyNotificationFixture())
248+
247249
def _restore_obj_registry(self):
248250
objects_base.NovaObjectRegistry._registry._obj_classes = \
249251
self._base_test_obj_backup

nova/tests/fixtures.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,26 @@ def setUp(self):
524524

525525
def cleanup(self):
526526
self._ctx_manager._root_factory = self._existing_factory
527+
528+
529+
class ForbidNewLegacyNotificationFixture(fixtures.Fixture):
530+
"""Make sure the test fails if new legacy notification is added"""
531+
def __init__(self):
532+
super(ForbidNewLegacyNotificationFixture, self).__init__()
533+
self.notifier = rpc.LegacyValidatingNotifier
534+
535+
def setUp(self):
536+
super(ForbidNewLegacyNotificationFixture, self).setUp()
537+
self.notifier.fatal = True
538+
539+
# allow the special test value used in
540+
# nova.tests.unit.test_notifications.NotificationsTestCase
541+
self.notifier.allowed_legacy_notification_event_types.append(
542+
'_decorated_function')
543+
544+
self.addCleanup(self.cleanup)
545+
546+
def cleanup(self):
547+
self.notifier.fatal = False
548+
self.notifier.allowed_legacy_notification_event_types.remove(
549+
'_decorated_function')

nova/tests/unit/test_rpc.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ def test_get_notifier(self):
222222
notifier = rpc.get_notifier('service', publisher_id='foo')
223223

224224
mock_prep.assert_called_once_with(publisher_id='foo')
225-
self.assertEqual('notifier', notifier)
225+
self.assertIsInstance(notifier, rpc.LegacyValidatingNotifier)
226+
self.assertEqual('notifier', notifier.notifier)
226227

227228
def test_get_notifier_null_publisher(self):
228229
rpc.LEGACY_NOTIFIER = mock.Mock()
@@ -233,7 +234,8 @@ def test_get_notifier_null_publisher(self):
233234
notifier = rpc.get_notifier('service', host='bar')
234235

235236
mock_prep.assert_called_once_with(publisher_id='service.bar')
236-
self.assertEqual('notifier', notifier)
237+
self.assertIsInstance(notifier, rpc.LegacyValidatingNotifier)
238+
self.assertEqual('notifier', notifier.notifier)
237239

238240
def test_get_versioned_notifier(self):
239241
rpc.NOTIFIER = mock.Mock()

0 commit comments

Comments
 (0)