Merge lp:~cerberus/nova/nova_notifications into lp:~hudson-openstack/nova/trunk

Proposed by Matt Dietz
Status: Merged
Approved by: Josh Kearney
Approved revision: 775
Merged at revision: 1086
Proposed branch: lp:~cerberus/nova/nova_notifications
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 347 lines (+306/-0)
7 files modified
nova/flags.py (+3/-0)
nova/notifier/__init__.py (+14/-0)
nova/notifier/api.py (+83/-0)
nova/notifier/log_notifier.py (+34/-0)
nova/notifier/no_op_notifier.py (+19/-0)
nova/notifier/rabbit_notifier.py (+36/-0)
nova/tests/test_notifier.py (+117/-0)
To merge this branch: bzr merge lp:~cerberus/nova/nova_notifications
Reviewer Review Type Date Requested Status
Josh Kearney (community) Approve
Brian Waldon (community) Approve
Jay Pipes (community) Approve
Rick Harris (community) Approve
Review via email: mp+58825@code.launchpad.net

Description of the change

Implements a basic mechanism for pushing notifications out to interested parties. The rationale for implementing notifications this way is that the responsibility for them shouldn't fall to Nova. As such, we simply will be pushing messages to a queue where another worker entirely can be written to push messages around to subscribers.

To post a comment you must log in.
Revision history for this message
Jay Pipes (jaypipes) wrote :

Looks reasonable as a starting point for a watch system. One request, though, could you elaborate in the docstrings what the "model" is expected to be? Mapping? Object? A string?

review: Needs Information
Revision history for this message
Matt Dietz (cerberus) wrote :

Absolutely Jay. I'll get on that.

Revision history for this message
Vish Ishaya (vishvananda) wrote :
Download full text (7.5 KiB)

FWIW I really hate importing things in __init__.py because it leads to strange import chains where things get imported when you didn't expect them to. Is there a reason to put this call here instead of for example in notify/api.py?

Vish

On Apr 22, 2011, at 8:41 AM, Matt Dietz wrote:

> Matt Dietz has proposed merging lp:~cerberus/nova/nova_notifications into lp:nova.
>
> Requested reviews:
> Nova Core (nova-core)
>
> For more details, see:
> https://code.launchpad.net/~cerberus/nova/nova_notifications/+merge/58825
>
> Implements a basic mechanism for pushing notifications out to interested parties. The rationale for implementing notifications this way is that the responsibility for them shouldn't fall to Nova. As such, we simply will be pushing messages to a queue where another worker entirely can be written to push messages around to subscribers.
> --
> https://code.launchpad.net/~cerberus/nova/nova_notifications/+merge/58825
> You are subscribed to branch lp:nova.
> === modified file 'nova/flags.py'
> --- nova/flags.py 2011-04-21 23:01:38 +0000
> +++ nova/flags.py 2011-04-22 15:41:24 +0000
> @@ -369,6 +369,9 @@
> DEFINE_string('node_availability_zone', 'nova',
> 'availability zone of this node')
>
> +DEFINE_string('notification_driver',
> + 'nova.notifier.no_op_notifier.NoopNotifier',
> + 'Default driver for sending notifications')
> DEFINE_string('zone_name', 'nova', 'name of this zone')
> DEFINE_list('zone_capabilities',
> ['hypervisor=xenserver;kvm', 'os=linux;windows'],
>
> === added directory 'nova/notifier'
> === added file 'nova/notifier/__init__.py'
> --- nova/notifier/__init__.py 1970-01-01 00:00:00 +0000
> +++ nova/notifier/__init__.py 2011-04-22 15:41:24 +0000
> @@ -0,0 +1,24 @@
> +# Copyright 2011 OpenStack LLC.
> +# 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 nova import flags
> +from nova import utils
> +
> +FLAGS = flags.FLAGS
> +
> +def notify(event_name, model):
> + """Sends a notification using the specified driver"""
> + driver = utils.import_class(FLAGS.notification_driver)()
> + driver.notify(event_name, model)
>
> === added file 'nova/notifier/no_op_notifier.py'
> --- nova/notifier/no_op_notifier.py 1970-01-01 00:00:00 +0000
> +++ nova/notifier/no_op_notifier.py 2011-04-22 15:41:24 +0000
> @@ -0,0 +1,19 @@
> +# Copyright 2011 OpenStack LLC.
> +# 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...

Read more...

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Looking good.

review: Approve
Revision history for this message
Matt Dietz (cerberus) wrote :

I'm planning to revamp this branch with feedback obtained at the summit. Changes forthcoming...

Revision history for this message
Jay Pipes (jaypipes) wrote :

Nice improvements, Matt!

Little nits:

145 + """ log notifications using nova's default logging system """

Please remove the spaces before and after enclosing """

107 + raise BadPriorityException('%s not in valid priorities' % priority)

Need i18n in the string there...

Other than those tiny things, looks excellent.

Cheers,
jay

review: Needs Fixing
Revision history for this message
Brian Waldon (bcwaldon) wrote :

147-154, 179, 217-224: Can we use "message" instead of "payload"? The naming is a little confusing.

Would you mind documenting the notifier drivers a bit better through a base class?

review: Needs Fixing
Revision history for this message
Matt Dietz (cerberus) wrote :

Thanks for the feedback, guys.

Jay: I think I've cleaned up what you suggested.

Brian: I made the message changes

I'm a little confused by the request to implement a base class here. Each notifier only has a single argument "notify" method(but the self reference), and that's all there would be to document amongst them. The functionality after that point is entirely different. Beyond that, I feel like the api.notify method is the real one to watch.

Regarding the idea of base classes, I'm opposed to the practice of implementing interfaces in Python. Given the "duck typing" nature of Python, it seems superfluous. If there was methods or member variables to be shared amongst them, it would of course be different.

I've noticed the practice in Nova is starting to lean the way of implementing interfaces, but the argument that's been made is that it's intended to catch missing functionality from driver classes. However, the right answer, in my opinion, is to ensure you've written tests to that implicit interface.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> Brian: I made the message changes
>
> I'm a little confused by the request to implement a base class here. Each
> notifier only has a single argument "notify" method(but the self reference),
> and that's all there would be to document amongst them. The functionality
> after that point is entirely different. Beyond that, I feel like the
> api.notify method is the real one to watch.
>
> Regarding the idea of base classes, I'm opposed to the practice of
> implementing interfaces in Python. Given the "duck typing" nature of Python,
> it seems superfluous. If there was methods or member variables to be shared
> amongst them, it would of course be different.
>
> I've noticed the practice in Nova is starting to lean the way of implementing
> interfaces, but the argument that's been made is that it's intended to catch
> missing functionality from driver classes. However, the right answer, in my
> opinion, is to ensure you've written tests to that implicit interface.

You're definitely right, and the reason I suggested it was more to help with documentation rather than enforcing interfaces. It took me longer than it should have to wrap my head around the system as a whole, and I thought it would help others get up to speed quicker. It really isn't a big deal, just an idea I wanted to run by you. I am ready to approve once the following are addressed:

78: you don't seem to use 'event_name', 'message' should be 'payload'

84: this line doesn't belong

90: 'message' should be 'payload'

92: remove 'payload'

103: 'message' should be 'payload'

114: 'message' should be 'payload'

review: Needs Fixing
Revision history for this message
Rick Harris (rconradharris) wrote :

Looks great.

Not really a nit, just an observation:

> 215 +class RabbitNotifier(object):
> 216 + """Sends notifications to a specific RabbitMQ server and topic"""
> 217 +
> 218 + def notify(self, message):

It looks like the Notifier /class/ is superflous since we define a single stateless function inside of it and we don't derive the implementations from a base-class.

Should we just make each driver's `notify` proc a function which is called by `api.notify`?

As a side benefit, we get to avoid the cost of instantiating an object for each notification sent (of which there will be a gazillion), as well as the small penalty of later having to gc them.

review: Approve
Revision history for this message
Matt Dietz (cerberus) wrote :

Good catches Brian. No clue how I messed that up!

Rick: Very good point. Let me rework this a bit...

Revision history for this message
Matt Dietz (cerberus) wrote :

84: this line doesn't belong

Rewrote the comment to better explain what I meant by including that.

Think I covered the rest. Also made changes based on Rick's suggestion

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> Think I covered the rest. Also made changes based on Rick's suggestion

78, 88, 118: I still feel like 'message' on these lines should be 'payload'. It's confusing to give the notify function a 'message' which is encapsulated as the 'payload' in a new type of 'message' structure.

lp:~cerberus/nova/nova_notifications updated
773. By Matt Dietz

Conceded :-D

Revision history for this message
Jay Pipes (jaypipes) wrote :

lgtm.

review: Approve
Revision history for this message
Rick Harris (rconradharris) wrote :

Brian:

Looks like patch has been updated to use 'payload', if it looks good, minding throwing an 'Approve' on there?

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Thanks for the work, Matt. Sorry to be so picky.

review: Approve
Revision history for this message
Chris Behrens (cbehrens) wrote :

Looks pretty awesome.

Minor nit that I notice in the tests:

test_send_notification uses self.notify_called when you don't need to set this in the instance. Ie, you can just do:

notify_called = False

def mock_notify(cls, *args):
    notify_called = True

Similar thing in the other tests. self.mock_cast and self.test_topic.

Revision history for this message
Josh Kearney (jk0) wrote :

Per HACKING, I would stick an extra newline after #59, #145 and #208.

lp:~cerberus/nova/nova_notifications updated
774. By Matt Dietz

Merge from trunk

775. By Matt Dietz

Spacing changes

Revision history for this message
Matt Dietz (cerberus) wrote :

Chris: that actually doesn't work because of the way Python handles scoping in assignments. Your way would create variables scope local to the mock method only, so there would be no way of checking them later in the test.

Josh: Spacing changes made.

Revision history for this message
Josh Kearney (jk0) wrote :

LGTM!

review: Approve
Revision history for this message
Rick Harris (rconradharris) wrote :

> Your way would create variables scope local to the mock method only, so there would be no way of checking them later in the test.

A bit of a hack, but, one way around this is to create a locally-scoped dict which the mocked function can mutate.

monitored = dict(notified=False)
def fake_notify():
    monitored['notified'] = True

self.assertTrue(monitored['notified])

(* Neutral on whether it should be used in this case *)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'nova/flags.py'
--- nova/flags.py 2011-04-29 22:06:18 +0000
+++ nova/flags.py 2011-05-18 17:56:56 +0000
@@ -369,6 +369,9 @@
369DEFINE_string('node_availability_zone', 'nova',369DEFINE_string('node_availability_zone', 'nova',
370 'availability zone of this node')370 'availability zone of this node')
371371
372DEFINE_string('notification_driver',
373 'nova.notifier.no_op_notifier',
374 'Default driver for sending notifications')
372DEFINE_list('memcached_servers', None,375DEFINE_list('memcached_servers', None,
373 'Memcached servers or None for in process cache.')376 'Memcached servers or None for in process cache.')
374377
375378
=== added directory 'nova/notifier'
=== added file 'nova/notifier/__init__.py'
--- nova/notifier/__init__.py 1970-01-01 00:00:00 +0000
+++ nova/notifier/__init__.py 2011-05-18 17:56:56 +0000
@@ -0,0 +1,14 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
015
=== added file 'nova/notifier/api.py'
--- nova/notifier/api.py 1970-01-01 00:00:00 +0000
+++ nova/notifier/api.py 2011-05-18 17:56:56 +0000
@@ -0,0 +1,83 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.import datetime
15
16import datetime
17import uuid
18
19from nova import flags
20from nova import utils
21
22
23FLAGS = flags.FLAGS
24
25flags.DEFINE_string('default_notification_level', 'INFO',
26 'Default notification level for outgoing notifications')
27
28WARN = 'WARN'
29INFO = 'INFO'
30ERROR = 'ERROR'
31CRITICAL = 'CRITICAL'
32DEBUG = 'DEBUG'
33
34log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL)
35
36
37class BadPriorityException(Exception):
38 pass
39
40
41def notify(publisher_id, event_type, priority, payload):
42 """
43 Sends a notification using the specified driver
44
45 Notify parameters:
46
47 publisher_id - the source worker_type.host of the message
48 event_type - the literal type of event (ex. Instance Creation)
49 priority - patterned after the enumeration of Python logging levels in
50 the set (DEBUG, WARN, INFO, ERROR, CRITICAL)
51 payload - A python dictionary of attributes
52
53 Outgoing message format includes the above parameters, and appends the
54 following:
55
56 message_id - a UUID representing the id for this notification
57 timestamp - the GMT timestamp the notification was sent at
58
59 The composite message will be constructed as a dictionary of the above
60 attributes, which will then be sent via the transport mechanism defined
61 by the driver.
62
63 Message example:
64
65 {'message_id': str(uuid.uuid4()),
66 'publisher_id': 'compute.host1',
67 'timestamp': datetime.datetime.utcnow(),
68 'priority': 'WARN',
69 'event_type': 'compute.create_instance',
70 'payload': {'instance_id': 12, ... }}
71
72 """
73 if priority not in log_levels:
74 raise BadPriorityException(
75 _('%s not in valid priorities' % priority))
76 driver = utils.import_object(FLAGS.notification_driver)
77 msg = dict(message_id=str(uuid.uuid4()),
78 publisher_id=publisher_id,
79 event_type=event_type,
80 priority=priority,
81 payload=payload,
82 timestamp=str(datetime.datetime.utcnow()))
83 driver.notify(msg)
084
=== added file 'nova/notifier/log_notifier.py'
--- nova/notifier/log_notifier.py 1970-01-01 00:00:00 +0000
+++ nova/notifier/log_notifier.py 2011-05-18 17:56:56 +0000
@@ -0,0 +1,34 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import json
17
18from nova import flags
19from nova import log as logging
20
21
22FLAGS = flags.FLAGS
23
24
25def notify(message):
26 """Notifies the recipient of the desired event given the model.
27 Log notifications using nova's default logging system"""
28
29 priority = message.get('priority',
30 FLAGS.default_notification_level)
31 priority = priority.lower()
32 logger = logging.getLogger(
33 'nova.notification.%s' % message['event_type'])
34 getattr(logger, priority)(json.dumps(message))
035
=== added file 'nova/notifier/no_op_notifier.py'
--- nova/notifier/no_op_notifier.py 1970-01-01 00:00:00 +0000
+++ nova/notifier/no_op_notifier.py 2011-05-18 17:56:56 +0000
@@ -0,0 +1,19 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16
17def notify(message):
18 """Notifies the recipient of the desired event given the model"""
19 pass
020
=== added file 'nova/notifier/rabbit_notifier.py'
--- nova/notifier/rabbit_notifier.py 1970-01-01 00:00:00 +0000
+++ nova/notifier/rabbit_notifier.py 2011-05-18 17:56:56 +0000
@@ -0,0 +1,36 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16
17import nova.context
18
19from nova import flags
20from nova import rpc
21
22
23FLAGS = flags.FLAGS
24
25flags.DEFINE_string('notification_topic', 'notifications',
26 'RabbitMQ topic used for Nova notifications')
27
28
29def notify(message):
30 """Sends a notification to the RabbitMQ"""
31 context = nova.context.get_admin_context()
32 priority = message.get('priority',
33 FLAGS.default_notification_level)
34 priority = priority.lower()
35 topic = '%s.%s' % (FLAGS.notification_topic, priority)
36 rpc.cast(context, topic, message)
037
=== added file 'nova/tests/test_notifier.py'
--- nova/tests/test_notifier.py 1970-01-01 00:00:00 +0000
+++ nova/tests/test_notifier.py 2011-05-18 17:56:56 +0000
@@ -0,0 +1,117 @@
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import nova
17
18from nova import context
19from nova import flags
20from nova import rpc
21import nova.notifier.api
22from nova.notifier.api import notify
23from nova.notifier import no_op_notifier
24from nova.notifier import rabbit_notifier
25from nova import test
26
27import stubout
28
29
30class NotifierTestCase(test.TestCase):
31 """Test case for notifications"""
32 def setUp(self):
33 super(NotifierTestCase, self).setUp()
34 self.stubs = stubout.StubOutForTesting()
35
36 def tearDown(self):
37 self.stubs.UnsetAll()
38 super(NotifierTestCase, self).tearDown()
39
40 def test_send_notification(self):
41 self.notify_called = False
42
43 def mock_notify(cls, *args):
44 self.notify_called = True
45
46 self.stubs.Set(nova.notifier.no_op_notifier, 'notify',
47 mock_notify)
48
49 class Mock(object):
50 pass
51 notify('publisher_id', 'event_type',
52 nova.notifier.api.WARN, dict(a=3))
53 self.assertEqual(self.notify_called, True)
54
55 def test_verify_message_format(self):
56 """A test to ensure changing the message format is prohibitively
57 annoying"""
58
59 def message_assert(message):
60 fields = [('publisher_id', 'publisher_id'),
61 ('event_type', 'event_type'),
62 ('priority', 'WARN'),
63 ('payload', dict(a=3))]
64 for k, v in fields:
65 self.assertEqual(message[k], v)
66 self.assertTrue(len(message['message_id']) > 0)
67 self.assertTrue(len(message['timestamp']) > 0)
68
69 self.stubs.Set(nova.notifier.no_op_notifier, 'notify',
70 message_assert)
71 notify('publisher_id', 'event_type',
72 nova.notifier.api.WARN, dict(a=3))
73
74 def test_send_rabbit_notification(self):
75 self.stubs.Set(nova.flags.FLAGS, 'notification_driver',
76 'nova.notifier.rabbit_notifier')
77 self.mock_cast = False
78
79 def mock_cast(cls, *args):
80 self.mock_cast = True
81
82 class Mock(object):
83 pass
84
85 self.stubs.Set(nova.rpc, 'cast', mock_cast)
86 notify('publisher_id', 'event_type',
87 nova.notifier.api.WARN, dict(a=3))
88
89 self.assertEqual(self.mock_cast, True)
90
91 def test_invalid_priority(self):
92 def mock_cast(cls, *args):
93 pass
94
95 class Mock(object):
96 pass
97
98 self.stubs.Set(nova.rpc, 'cast', mock_cast)
99 self.assertRaises(nova.notifier.api.BadPriorityException,
100 notify, 'publisher_id',
101 'event_type', 'not a priority', dict(a=3))
102
103 def test_rabbit_priority_queue(self):
104 self.stubs.Set(nova.flags.FLAGS, 'notification_driver',
105 'nova.notifier.rabbit_notifier')
106 self.stubs.Set(nova.flags.FLAGS, 'notification_topic',
107 'testnotify')
108
109 self.test_topic = None
110
111 def mock_cast(context, topic, msg):
112 self.test_topic = topic
113
114 self.stubs.Set(nova.rpc, 'cast', mock_cast)
115 notify('publisher_id',
116 'event_type', 'DEBUG', dict(a=3))
117 self.assertEqual(self.test_topic, 'testnotify.debug')