From a21c8a555d1c1ed988d23cc4b4665d19a4132f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Sedl=C3=A1=C5=99?= Date: Wed, 28 Jun 2017 10:26:29 +0200 Subject: [PATCH] notification: Allow specifying multiple scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notification hooks can be useful for doing other things than just announcing status on message bus. For this to be truly usable, we need the ability to use multiple scripts. This patch allows the command line option to be specified multiple times. Each given script will be called. Even if the script fails, it does not block the compose. Additionally the output of the notification scripts is logged now to make it possible to debug possible failure. Relates: https://pagure.io/pungi/issue/650 Signed-off-by: Lubomír Sedlář --- bin/pungi-koji | 2 + pungi/notifier.py | 43 +++++++++----- tests/test_notifier.py | 124 +++++++++++++++++++++++------------------ 3 files changed, 102 insertions(+), 67 deletions(-) diff --git a/bin/pungi-koji b/bin/pungi-koji index 4b0a4836..7b0bd842 100755 --- a/bin/pungi-koji +++ b/bin/pungi-koji @@ -126,6 +126,8 @@ def main(): ) parser.add_argument( "--notification-script", + action="append", + default=[], help="script for sending progress notification messages" ) parser.add_argument( diff --git a/pungi/notifier.py b/pungi/notifier.py index 3e56d21b..282a9ee0 100644 --- a/pungi/notifier.py +++ b/pungi/notifier.py @@ -12,7 +12,9 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +from datetime import datetime import json +import os import threading import pungi.util @@ -27,8 +29,8 @@ class PungiNotifier(object): script fails, a warning will be logged, but the compose process will not be interrupted. """ - def __init__(self, cmd): - self.cmd = cmd + def __init__(self, cmds): + self.cmds = cmds self.lock = threading.Lock() self.compose = None @@ -53,7 +55,7 @@ class PungiNotifier(object): Unless you specify it manually, a ``compose_id`` key with appropriate value will be automatically added. """ - if not self.cmd: + if not self.cmds: return self._update_args(kwargs) @@ -62,14 +64,29 @@ class PungiNotifier(object): workdir = self.compose.paths.compose.topdir() with self.lock: + for cmd in self.cmds: + self._run_script(cmd, msg, workdir, kwargs) + + def _run_script(self, cmd, msg, workdir, kwargs): + """Run a single notification script with proper logging.""" + logfile = None + if self.compose: + self.compose.log_debug("Notification: %r %r, %r" % ( + cmd, msg, kwargs)) + logfile = os.path.join( + self.compose.paths.log.topdir(), + 'notifications', + 'notification-%s.log' % datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S') + ) + pungi.util.makedirs(os.path.dirname(logfile)) + + ret, _ = shortcuts.run((cmd, msg), + stdin_data=json.dumps(kwargs), + can_fail=True, + workdir=workdir, + return_stdout=False, + show_cmd=True, + logfile=logfile) + if ret != 0: if self.compose: - self.compose.log_debug("Notification: %r %r, %r" % ( - self.cmd, msg, kwargs)) - ret, _ = shortcuts.run((self.cmd, msg), - stdin_data=json.dumps(kwargs), - can_fail=True, - workdir=workdir, - return_stdout=False) - if ret != 0: - if self.compose: - self.compose.log_warning('Failed to invoke notification script.') + self.compose.log_warning('Failed to invoke notification script.') diff --git a/tests/test_notifier.py b/tests/test_notifier.py index 282189da..13febfe8 100644 --- a/tests/test_notifier.py +++ b/tests/test_notifier.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from datetime import datetime import json import mock import os @@ -12,91 +13,106 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from pungi.notifier import PungiNotifier +mock_datetime = mock.Mock() +mock_datetime.utcnow.return_value = datetime(2017, 6, 28, 9, 34) +mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + + +@mock.patch('pungi.util.makedirs') +@mock.patch('pungi.notifier.datetime', new=mock_datetime) class TestNotifier(unittest.TestCase): - @mock.patch('pungi.util.translate_path') - @mock.patch('kobo.shortcuts.run') - def test_invokes_script(self, run, translate_path): - compose = mock.Mock( + + def setUp(self): + super(TestNotifier, self).setUp() + self.logfile = '/logs/notifications/notification-2017-06-28_09-34-00.log' + self.compose = mock.Mock( compose_id='COMPOSE_ID', + log_warning=mock.Mock(), paths=mock.Mock( compose=mock.Mock( topdir=mock.Mock(return_value='/a/b') + ), + log=mock.Mock( + topdir=mock.Mock(return_value='/logs') ) ) ) + self.data = {'foo': 'bar', 'baz': 'quux'} + def _call(self, script, cmd, **kwargs): + data = self.data.copy() + data['compose_id'] = 'COMPOSE_ID' + data['location'] = '/a/b' + data.update(kwargs) + return mock.call((script, cmd), + stdin_data=json.dumps(data), + can_fail=True, return_stdout=False, + workdir=self.compose.paths.compose.topdir.return_value, + show_cmd=True, logfile=self.logfile) + + @mock.patch('pungi.util.translate_path') + @mock.patch('kobo.shortcuts.run') + def test_invokes_script(self, run, translate_path, makedirs): run.return_value = (0, None) translate_path.side_effect = lambda compose, x: x - n = PungiNotifier('run-notify') - n.compose = compose - data = {'foo': 'bar', 'baz': 'quux'} - n.send('cmd', **data) + n = PungiNotifier(['run-notify']) + n.compose = self.compose + n.send('cmd', **self.data) - data['compose_id'] = 'COMPOSE_ID' - data['location'] = '/a/b' - run.assert_called_once_with(('run-notify', 'cmd'), - stdin_data=json.dumps(data), - can_fail=True, return_stdout=False, workdir='/a/b') + makedirs.assert_called_once_with('/logs/notifications') + self.assertItemsEqual(run.call_args_list, [self._call('run-notify', 'cmd')]) + + @mock.patch('pungi.util.translate_path') + @mock.patch('kobo.shortcuts.run') + def test_invokes_multiple_scripts(self, run, translate_path, makedirs): + run.return_value = (0, None) + translate_path.side_effect = lambda compose, x: x + + n = PungiNotifier(['run-notify', 'ping-user']) + n.compose = self.compose + n.send('cmd', **self.data) + + self.assertItemsEqual( + run.call_args_list, + [self._call('run-notify', 'cmd'), + self._call('ping-user', 'cmd')]) @mock.patch('kobo.shortcuts.run') - def test_translates_path(self, run): - compose = mock.Mock( - compose_id='COMPOSE_ID', - paths=mock.Mock( - compose=mock.Mock( - topdir=mock.Mock(return_value='/root/a/b') - ) - ), - conf={ - "translate_paths": [("/root/", "http://example.com/compose/")], - } - ) + def test_translates_path(self, run, makedirs): + self.compose.paths.compose.topdir.return_value = '/root/a/b' + self.compose.conf = { + "translate_paths": [("/root/", "http://example.com/compose/")], + } run.return_value = (0, None) - n = PungiNotifier('run-notify') - n.compose = compose - data = {'foo': 'bar', 'baz': 'quux'} - n.send('cmd', **data) + n = PungiNotifier(['run-notify']) + n.compose = self.compose + n.send('cmd', **self.data) - data['compose_id'] = 'COMPOSE_ID' - data['location'] = 'http://example.com/compose/a/b' - run.assert_called_once_with(('run-notify', 'cmd'), - stdin_data=json.dumps(data), - can_fail=True, return_stdout=False, - workdir='/root/a/b') + self.assertItemsEqual( + run.call_args_list, + [self._call('run-notify', 'cmd', location='http://example.com/compose/a/b')]) @mock.patch('kobo.shortcuts.run') - def test_does_not_run_without_config(self, run): + def test_does_not_run_without_config(self, run, makedirs): n = PungiNotifier(None) n.send('cmd', foo='bar', baz='quux') self.assertFalse(run.called) @mock.patch('pungi.util.translate_path') @mock.patch('kobo.shortcuts.run') - def test_logs_warning_on_failure(self, run, translate_path): - compose = mock.Mock( - compose_id='COMPOSE_ID', - log_warning=mock.Mock(), - paths=mock.Mock( - compose=mock.Mock( - topdir=mock.Mock(return_value='/a/b') - ) - ) - ) - + def test_logs_warning_on_failure(self, run, translate_path, makedirs): translate_path.side_effect = lambda compose, x: x run.return_value = (1, None) - n = PungiNotifier('run-notify') - n.compose = compose - n.send('cmd') + n = PungiNotifier(['run-notify']) + n.compose = self.compose + n.send('cmd', **self.data) - run.assert_called_once_with(('run-notify', 'cmd'), - stdin_data=json.dumps({'compose_id': 'COMPOSE_ID', 'location': '/a/b'}), - can_fail=True, return_stdout=False, workdir='/a/b') - self.assertTrue(compose.log_warning.called) + self.assertItemsEqual(run.call_args_list, [self._call('run-notify', 'cmd')]) + self.assertTrue(self.compose.log_warning.called) if __name__ == "__main__":