Merge lp:~blamar/glance/notifier-strategy into lp:~johannes.erdfelt/glance/notifier

Proposed by Brian Lamar
Status: Merged
Merged at revision: 169
Proposed branch: lp:~blamar/glance/notifier-strategy
Merge into: lp:~johannes.erdfelt/glance/notifier
Diff against target: 707 lines (+256/-247)
13 files modified
Authors (+1/-0)
bin/glance-api (+0/-3)
bin/glance-registry (+0/-3)
glance/api/v1/images.py (+8/-7)
glance/common/client.py (+6/-0)
glance/common/exception.py (+4/-0)
glance/common/notifier.py (+115/-135)
tests/functional/test_scrubber.py (+20/-4)
tests/stubs.py (+0/-8)
tests/unit/test_api.py (+0/-1)
tests/unit/test_clients.py (+0/-1)
tests/unit/test_notifier.py (+101/-85)
tools/pip-requires (+1/-0)
To merge this branch: bzr merge lp:~blamar/glance/notifier-strategy
Reviewer Review Type Date Requested Status
Johannes Erdfelt Approve
Review via email: mp+69341@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

I like the direction of your changes. The code was a port of the Tempo notification code, which in turn was a port of the nova notification code, but it was a mistake not to take the opportunity and consider if it could be simpler.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Authors'
2--- Authors 2011-07-26 04:24:28 +0000
3+++ Authors 2011-07-26 18:44:34 +0000
4@@ -1,4 +1,5 @@
5 Andrey Brindeyev <abrindeyev@griddynamics.com>
6+Brian Lamar <brian.lamar@rackspace.com>
7 Brian Waldon <brian.waldon@rackspace.com>
8 Christopher MacGown <chris@slicehost.com>
9 Cory Wright <corywright@gmail.com>
10
11=== modified file 'bin/glance-api'
12--- bin/glance-api 2011-07-22 17:59:11 +0000
13+++ bin/glance-api 2011-07-26 18:44:34 +0000
14@@ -36,7 +36,6 @@
15
16 from glance import version
17 from glance.common import config
18-from glance.common import notifier
19 from glance.common import wsgi
20
21
22@@ -60,8 +59,6 @@
23 try:
24 conf, app = config.load_paste_app('glance-api', options, args)
25
26- notifier.configure_notifier(conf)
27-
28 server = wsgi.Server()
29 server.start(app, int(conf['bind_port']), conf['bind_host'])
30 server.wait()
31
32=== modified file 'bin/glance-registry'
33--- bin/glance-registry 2011-07-22 17:59:11 +0000
34+++ bin/glance-registry 2011-07-26 18:44:34 +0000
35@@ -36,7 +36,6 @@
36
37 from glance import version
38 from glance.common import config
39-from glance.common import notifier
40 from glance.common import wsgi
41
42
43@@ -60,8 +59,6 @@
44 try:
45 conf, app = config.load_paste_app('glance-registry', options, args)
46
47- notifier.configure_notifier(conf)
48-
49 server = wsgi.Server()
50 server.start(app, int(conf['bind_port']), conf['bind_host'])
51 server.wait()
52
53=== modified file 'glance/api/v1/images.py'
54--- glance/api/v1/images.py 2011-07-26 04:48:33 +0000
55+++ glance/api/v1/images.py 2011-07-26 18:44:34 +0000
56@@ -73,6 +73,7 @@
57
58 def __init__(self, options):
59 self.options = options
60+ self.notifier = notifier.Notifier(options)
61
62 def index(self, req):
63 """
64@@ -354,7 +355,7 @@
65 image_id,
66 {'checksum': checksum,
67 'size': size})
68- notifier.notify('image.upload', 'INFO', image_meta)
69+ self.notifier.info('image.upload', image_meta)
70
71 return location
72
73@@ -362,14 +363,14 @@
74 msg = ("Attempt to upload duplicate image: %s") % str(e)
75 logger.error(msg)
76 self._safe_kill(req, image_id)
77- notifier.notify('image.upload', 'ERROR', msg)
78+ self.notifier.error('image.upload', msg)
79 raise HTTPConflict(msg, request=req)
80
81 except exception.NotAuthorized, e:
82 msg = ("Unauthorized upload attempt: %s") % str(e)
83 logger.error(msg)
84 self._safe_kill(req, image_id)
85- notifier.notify('image.upload', 'ERROR', msg)
86+ self.notifier.error('image.upload', msg)
87 raise HTTPForbidden(msg, request=req,
88 content_type='text/plain')
89
90@@ -377,7 +378,7 @@
91 msg = ("Error uploading image: %s") % str(e)
92 logger.error(msg)
93 self._safe_kill(req, image_id)
94- notifier.notify('image.upload', 'ERROR', msg)
95+ self.notifier.error('image.upload', msg)
96 raise HTTPBadRequest(msg, request=req)
97
98 def _activate(self, req, image_id, location):
99@@ -524,10 +525,10 @@
100 % locals())
101 for line in msg.split('\n'):
102 logger.error(line)
103- notifier.notify('image.update', 'ERROR', msg)
104+ self.notifier.error('image.update', msg)
105 raise HTTPBadRequest(msg, request=req, content_type="text/plain")
106 else:
107- notifier.notify('image.update', 'INFO', image_meta)
108+ self.notifier.info('image.update', image_meta)
109
110 return {'image_meta': image_meta}
111
112@@ -559,7 +560,7 @@
113 schedule_delete_from_backend(image['location'], self.options,
114 req.context, id)
115 registry.delete_image_metadata(self.options, req.context, id)
116- notifier.notify('image.delete', 'INFO', id)
117+ self.notifier.info('image.delete', id)
118
119 def get_store_or_400(self, request, store_name):
120 """
121
122=== modified file 'glance/common/client.py'
123--- glance/common/client.py 2011-07-20 22:53:44 +0000
124+++ glance/common/client.py 2011-07-26 18:44:34 +0000
125@@ -56,6 +56,12 @@
126 self.auth_tok = auth_tok
127 self.connection = None
128
129+ def set_auth_token(self, auth_tok):
130+ """
131+ Updates the authentication token for this client connection.
132+ """
133+ self.auth_tok = auth_tok
134+
135 def get_connection_type(self):
136 """
137 Returns the proper connection type
138
139=== modified file 'glance/common/exception.py'
140--- glance/common/exception.py 2011-07-13 20:15:46 +0000
141+++ glance/common/exception.py 2011-07-26 18:44:34 +0000
142@@ -145,3 +145,7 @@
143
144 class InvalidContentType(GlanceException):
145 message = "Invalid content type %(content_type)s"
146+
147+
148+class InvalidNotifierStrategy(GlanceException):
149+ message = "'%(strategy)s' is not an available notifier strategy."
150
151=== modified file 'glance/common/notifier.py'
152--- glance/common/notifier.py 2011-07-26 17:11:27 +0000
153+++ glance/common/notifier.py 2011-07-26 18:44:34 +0000
154@@ -20,148 +20,128 @@
155 import socket
156 import uuid
157
158-from kombu.connection import BrokerConnection
159+import kombu.connection
160
161 from glance.common import config
162-
163-
164-WARN = 'WARN'
165-INFO = 'INFO'
166-ERROR = 'ERROR'
167-CRITICAL = 'CRITICAL'
168-DEBUG = 'DEBUG'
169-
170-log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL)
171-
172-_DRIVER = None
173-
174-
175-class BadPriorityException(Exception):
176- pass
177-
178-
179-def configure_notifier(options):
180- global _DRIVER
181- notification_driver = config.get_option(options, 'notification_driver',
182- type='str', default='logging')
183- _DRIVER = _get_notifier_driver(notification_driver)(options)
184-
185-
186-def notify(event_type, priority, payload):
187- """
188- Sends a notification using the specified driver
189-
190- Notify parameters:
191-
192- event_type - the literal type of event (ex. Instance Creation)
193- priority - patterned after the enumeration of Python logging levels in
194- the set (DEBUG, WARN, INFO, ERROR, CRITICAL)
195- payload - A python dictionary of attributes
196-
197- Outgoing message format includes the above parameters, and appends the
198- following:
199-
200- message_id - a UUID representing the id for this notification
201- timestamp - the GMT timestamp the notification was sent at
202-
203- The composite message will be constructed as a dictionary of the above
204- attributes, which will then be sent via the transport mechanism defined
205- by the driver.
206-
207- Message example:
208-
209- {'message_id': str(uuid.uuid4()),
210- 'publisher_id': 'compute.host1',
211- 'timestamp': utils.utcnow(),
212- 'priority': 'WARN',
213- 'event_type': 'compute.create_instance',
214- 'payload': {'instance_id': 12, ... }}
215-
216- """
217- if priority not in log_levels:
218- raise BadPriorityException(
219- _('%s not in valid priorities' % priority))
220-
221- msg = dict(message_id=str(uuid.uuid4()),
222- publisher_id=socket.gethostname(),
223- event_type=event_type,
224- priority=priority,
225- payload=payload,
226- timestamp=str(datetime.datetime.utcnow()))
227-
228- _DRIVER.notify(msg)
229-
230-
231-class Notifier(object):
232- def __init__(self, options):
233- self.level = config.get_option(options, 'default_notification_level',
234- type='str', default='INFO')
235-
236- def notify(self, msg):
237- raise NotImplementedError()
238-
239-
240-class NoopNotifier(Notifier):
241- def notify(self, msg):
242- pass
243-
244-
245-class LoggingNotifier(Notifier):
246- def __init__(self, options):
247- super(LoggingNotifier, self).__init__(options)
248- self._setup_logger()
249-
250- def _setup_logger(self):
251- str2log_level = {
252- 'DEBUG': logging.DEBUG,
253- 'INFO': logging.INFO,
254- 'WARN': logging.WARN,
255- 'ERROR': logging.ERROR,
256- 'CRITICAL': logging.CRITICAL}
257- self.level = str2log_level[self.level]
258+from glance.common import exception
259+
260+
261+class NoopStrategy(object):
262+ """A notifier that does nothing when called."""
263+
264+ def __init__(self, options):
265+ pass
266+
267+ def warn(self, msg):
268+ pass
269+
270+ def info(self, msg):
271+ pass
272+
273+ def error(self, msg):
274+ pass
275+
276+
277+class LoggingStrategy(object):
278+ """A notifier that calls logging when called."""
279+
280+ def __init__(self, options):
281 self.logger = logging.getLogger('glance.notifier.logging_notifier')
282
283- def notify(self, msg):
284- self.logger.log(self.level, msg)
285-
286-
287-class RabbitNotifier(Notifier):
288+ def warn(self, msg):
289+ self.logger.warn(msg)
290+
291+ def info(self, msg):
292+ self.logger.info(msg)
293+
294+ def error(self, msg):
295+ self.logger.error(msg)
296+
297+
298+class RabbitStrategy(object):
299+ """A notifier that puts a message on a queue when called."""
300+
301 def __init__(self, options):
302- super(RabbitNotifier, self).__init__(options)
303- host = config.get_option(options, 'rabbit_host',
304- type='str', default='localhost')
305- port = config.get_option(options, 'rabbit_port',
306- type='int', default=5672)
307- use_ssl = config.get_option(options, 'rabbit_use_ssl',
308- type='bool', default=False)
309- userid = config.get_option(options, 'rabbit_userid',
310- type='str', default='guest')
311- password = config.get_option(options, 'rabbit_password',
312- type='str', default='guest')
313- virtual_host = config.get_option(options, 'rabbit_virtual_host',
314- type='str', default='/')
315-
316- self.connection = BrokerConnection(
317- hostname=host,
318- userid=userid,
319- password=password,
320- virtual_host=virtual_host,
321- ssl=use_ssl)
322- self.topic = config.get_option(options, 'rabbit_notification_topic',
323- type='str', default='glance_notifications')
324-
325- def notify(self, message):
326- priority = message.get('priority', self.level)
327+ """Initialize the rabbit notification strategy."""
328+ self._options = options
329+ host = self._get_option('rabbit_host', 'str', 'localhost')
330+ port = self._get_option('rabbit_port', 'int', 5672)
331+ use_ssl = self._get_option('rabbit_use_ssl', 'bool', False)
332+ userid = self._get_option('rabbit_userid', 'str', 'guest')
333+ password = self._get_option('rabbit_password', 'str', 'guest')
334+ virtual_host = self._get_option('rabbit_virtual_host', 'str', '/')
335+
336+ self.connection = kombu.connection.BrokerConnection(
337+ hostname=host,
338+ userid=userid,
339+ password=password,
340+ virtual_host=virtual_host,
341+ ssl=use_ssl)
342+
343+ self.topic = self._get_option('rabbit_notification_topic',
344+ 'str',
345+ 'glance_notifications')
346+
347+ def _get_option(self, name, datatype, default):
348+ """Retrieve a configuration option."""
349+ return config.get_option(self._options,
350+ name,
351+ type=datatype,
352+ default=default)
353+
354+ def _send_message(self, message, priority):
355 topic = "%s.%s" % (self.topic, priority)
356 queue = self.connection.SimpleQueue(topic)
357 queue.put(message, serializer="json")
358 queue.close()
359
360-
361-def _get_notifier_driver(driver):
362- if driver == "logging":
363- return LoggingNotifier
364- elif driver == "rabbit":
365- return RabbitNotifier
366- else:
367- return NoopNotifier
368+ def warn(self, msg):
369+ self._send_message(msg, "WARN")
370+
371+ def info(self, msg):
372+ self._send_message(msg, "INFO")
373+
374+ def error(self, msg):
375+ self._send_message(msg, "ERROR")
376+
377+
378+class Notifier(object):
379+ """Uses a notification strategy to send out messages about events."""
380+
381+ STRATEGIES = {
382+ "logging": LoggingStrategy,
383+ "rabbit": RabbitStrategy,
384+ "noop": NoopStrategy,
385+ "default": NoopStrategy,
386+ }
387+
388+ def __init__(self, options, strategy=None):
389+ strategy = config.get_option(options, "notifier_strategy",
390+ type="str", default="default")
391+ try:
392+ self.strategy = self.STRATEGIES[strategy](options)
393+ except KeyError:
394+ raise exception.InvalidNotifierStrategy(strategy=strategy)
395+
396+ @staticmethod
397+ def generate_message(event_type, priority, payload):
398+ return {
399+ "message_id": str(uuid.uuid4()),
400+ "publisher_id": socket.gethostname(),
401+ "event_type": event_type,
402+ "priority": priority,
403+ "payload": payload,
404+ "timestamp": str(datetime.datetime.utcnow()),
405+ }
406+
407+ def warn(self, event_type, payload):
408+ msg = self.generate_message(event_type, "WARN", payload)
409+ self.strategy.warn(msg)
410+
411+ def info(self, event_type, payload):
412+ msg = self.generate_message(event_type, "INFO", payload)
413+ self.strategy.info(msg)
414+
415+ def error(self, event_type, payload):
416+ msg = self.generate_message(event_type, "ERROR", payload)
417+ self.strategy.error(msg)
418
419=== modified file 'tests/functional/test_scrubber.py'
420--- tests/functional/test_scrubber.py 2011-07-22 23:08:43 +0000
421+++ tests/functional/test_scrubber.py 2011-07-26 18:44:34 +0000
422@@ -100,10 +100,26 @@
423 for rec in recs:
424 self.assertEqual(rec['status'], 'pending_delete')
425
426- # Wait 15 seconds for the scrubber to scrub
427- time.sleep(15)
428-
429- recs = list(self.run_sql_cmd(sql))
430+ # NOTE(jkoelker) The build servers sometimes take longer than
431+ # 15 seconds to scrub. Give it up to 5 min, checking
432+ # checking every 15 seconds. When/if it flips to
433+ # deleted, bail immediatly.
434+ deleted = set()
435+ recs = []
436+ for _ in xrange(20):
437+ time.sleep(15)
438+
439+ recs = list(self.run_sql_cmd(sql))
440+ self.assertTrue(recs)
441+
442+ # NOTE(jkoelker) Reset the deleted set for this loop
443+ deleted = set()
444+ for rec in recs:
445+ deleted.add(rec['status'] == 'deleted')
446+
447+ if False not in deleted:
448+ break
449+
450 self.assertTrue(recs)
451 for rec in recs:
452 self.assertEqual(rec['status'], 'deleted')
453
454=== modified file 'tests/stubs.py'
455--- tests/stubs.py 2011-07-26 04:25:03 +0000
456+++ tests/stubs.py 2011-07-26 18:44:34 +0000
457@@ -31,7 +31,6 @@
458 import glance.common.client
459 from glance.common import context
460 from glance.common import exception
461-import glance.common.notifier
462 from glance.registry import server as rserver
463 from glance.api import v1 as server
464 import glance.store
465@@ -476,10 +475,3 @@
466 fake_datastore.image_get_all_pending_delete)
467 stubs.Set(glance.registry.db.api, 'image_get_all',
468 fake_datastore.image_get_all)
469-
470-
471-def stub_out_notifier(stubs):
472- def notify(event_type, priority, payload):
473- pass
474-
475- stubs.Set(glance.common.notifier, 'notify', notify)
476
477=== modified file 'tests/unit/test_api.py'
478--- tests/unit/test_api.py 2011-07-26 04:25:03 +0000
479+++ tests/unit/test_api.py 2011-07-26 18:44:34 +0000
480@@ -1451,7 +1451,6 @@
481 self.stubs = stubout.StubOutForTesting()
482 stubs.stub_out_registry_and_store_server(self.stubs)
483 stubs.stub_out_registry_db_image_api(self.stubs)
484- stubs.stub_out_notifier(self.stubs)
485 stubs.stub_out_filesystem_backend()
486 sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://")
487 options = {'verbose': VERBOSE,
488
489=== modified file 'tests/unit/test_clients.py'
490--- tests/unit/test_clients.py 2011-07-26 04:25:03 +0000
491+++ tests/unit/test_clients.py 2011-07-26 18:44:34 +0000
492@@ -897,7 +897,6 @@
493 stubs.stub_out_registry_db_image_api(self.stubs)
494 stubs.stub_out_registry_and_store_server(self.stubs)
495 stubs.stub_out_filesystem_backend()
496- stubs.stub_out_notifier(self.stubs)
497 self.client = client.Client("0.0.0.0", doc_root="")
498
499 def tearDown(self):
500
501=== modified file 'tests/unit/test_notifier.py'
502--- tests/unit/test_notifier.py 2011-07-26 13:58:04 +0000
503+++ tests/unit/test_notifier.py 2011-07-26 18:44:34 +0000
504@@ -15,93 +15,109 @@
505 # License for the specific language governing permissions and limitations
506 # under the License.
507
508-import os
509-import stubout
510+import logging
511 import unittest
512
513-from glance import client
514 from glance.common import exception
515-import glance.common.notifier
516-
517-from tests import stubs
518-
519-
520-TEST_IMAGE_META = {'name': 'test_image',
521- 'is_public': False,
522- 'disk_format': 'raw',
523- 'container_format': 'ovf'}
524-
525-
526-class TestNotifier(unittest.TestCase):
527+from glance.common import notifier
528+
529+
530+class TestInvalidNotifier(unittest.TestCase):
531 """Test that notifications are generated appropriately"""
532
533- def setUp(self):
534- """Establish a clean test environment"""
535- self.stubs = stubout.StubOutForTesting()
536- stubs.stub_out_registry_db_image_api(self.stubs)
537- stubs.stub_out_registry_and_store_server(self.stubs)
538- stubs.stub_out_filesystem_backend()
539- self.client = client.Client("0.0.0.0", doc_root="")
540- self.notification = (None, None)
541-
542- def notify(event_type, priority, payload):
543- self.notification = (event_type, priority)
544-
545- self.stubs.Set(glance.common.notifier, 'notify', notify)
546-
547- def tearDown(self):
548- """Clear the test environment"""
549- stubs.clean_out_fake_filesystem_backend()
550- self.stubs.UnsetAll()
551-
552- def test_create_notify(self):
553- """Test image create notification."""
554-
555- self.client.add_image(TEST_IMAGE_META, 'foobar')
556-
557- self.assertEquals(self.notification, ('image.upload', 'INFO'))
558-
559- def test_update_notify(self):
560- """Test image update notification. """
561-
562- image_meta = self.client.add_image(TEST_IMAGE_META, 'foobar')
563- self.client.update_image(image_meta['id'])
564-
565- self.assertEquals(self.notification, ('image.update', 'INFO'))
566-
567- def test_delete_notify(self):
568- """Test image delete notification. """
569-
570- image_meta = self.client.add_image(TEST_IMAGE_META, 'foobar')
571- self.client.delete_image(image_meta['id'])
572-
573- self.assertEquals(self.notification, ('image.delete', 'INFO'))
574-
575- def test_create_error_notify(self):
576- """Test image create error notification."""
577-
578- def boom(options, context, image_id, image_meta, purge_props=False):
579- if 'checksum' in image_meta:
580- raise Exception('boom')
581- return image_meta
582-
583- self.stubs.Set(glance.registry, 'update_image_metadata', boom)
584-
585- try:
586- self.client.add_image(TEST_IMAGE_META, 'foobar')
587- except exception.Invalid:
588- pass
589-
590- self.assertEquals(self.notification, ('image.upload', 'ERROR'))
591-
592- def test_update_error_notify(self):
593- """Test image update error notification."""
594-
595- image_meta = self.client.add_image(TEST_IMAGE_META, 'foobar')
596- try:
597- self.client.update_image(image_meta['id'],
598- {'container_format': 'invalid'})
599- except exception.Invalid:
600- pass
601-
602- self.assertEquals(self.notification, ('image.update', 'ERROR'))
603+ def test_cannot_create(self):
604+ options = {"notifier_strategy": "invalid_notifier"}
605+ self.assertRaises(exception.InvalidNotifierStrategy,
606+ notifier.Notifier,
607+ options)
608+
609+
610+class TestLoggingNotifier(unittest.TestCase):
611+ """Test the logging notifier is selected and works properly."""
612+
613+ def setUp(self):
614+ options = {"notifier_strategy": "logging"}
615+ self.called = False
616+ self.logger = logging.getLogger("glance.notifier.logging_notifier")
617+ self.notifier = notifier.Notifier(options)
618+
619+ def _called(self, msg):
620+ self.called = msg
621+
622+ def test_warn(self):
623+ self.logger.warn = self._called
624+ self.notifier.warn("test_event", "test_message")
625+ if self.called is False:
626+ self.fail("Did not call logging library correctly.")
627+
628+ def test_info(self):
629+ self.logger.info = self._called
630+ self.notifier.info("test_event", "test_message")
631+ if self.called is False:
632+ self.fail("Did not call logging library correctly.")
633+
634+ def test_erorr(self):
635+ self.logger.error = self._called
636+ self.notifier.error("test_event", "test_message")
637+ if self.called is False:
638+ self.fail("Did not call logging library correctly.")
639+
640+
641+class TestNoopNotifier(unittest.TestCase):
642+ """Test that the noop notifier works...and does nothing?"""
643+
644+ def setUp(self):
645+ options = {"notifier_strategy": "noop"}
646+ self.notifier = notifier.Notifier(options)
647+
648+ def test_warn(self):
649+ self.notifier.warn("test_event", "test_message")
650+
651+ def test_info(self):
652+ self.notifier.info("test_event", "test_message")
653+
654+ def test_error(self):
655+ self.notifier.error("test_event", "test_message")
656+
657+
658+class TestRabbitNotifier(unittest.TestCase):
659+ """Test AMQP/Rabbit notifier works."""
660+
661+ def setUp(self):
662+ notifier.RabbitStrategy._send_message = self._send_message
663+ self.called = False
664+ options = {"notifier_strategy": "rabbit"}
665+ self.notifier = notifier.Notifier(options)
666+
667+ def _send_message(self, message, priority):
668+ self.called = {
669+ "message": message,
670+ "priority": priority,
671+ }
672+
673+ def test_warn(self):
674+ self.notifier.warn("test_event", "test_message")
675+
676+ if self.called is False:
677+ self.fail("Did not call _send_message properly.")
678+
679+ self.assertEquals("test_message", self.called["message"]["payload"])
680+ self.assertEquals("WARN", self.called["message"]["priority"])
681+
682+ def test_info(self):
683+ self.notifier.info("test_event", "test_message")
684+
685+ if self.called is False:
686+ self.fail("Did not call _send_message properly.")
687+
688+ self.assertEquals("test_message", self.called["message"]["payload"])
689+ self.assertEquals("INFO", self.called["message"]["priority"])
690+
691+ def test_error(self):
692+ self.notifier.error("test_event", "test_message")
693+
694+ if self.called is False:
695+ self.fail("Did not call _send_message properly.")
696+
697+ self.assertEquals("test_message", self.called["message"]["payload"])
698+ self.assertEquals("ERROR", self.called["message"]["priority"])
699
700=== modified file 'tools/pip-requires'
701--- tools/pip-requires 2011-07-12 09:09:36 +0000
702+++ tools/pip-requires 2011-07-26 18:44:34 +0000
703@@ -19,3 +19,4 @@
704 httplib2
705 hashlib
706 xattr
707+kombu

Subscribers

People subscribed via source and target branches

to all changes: