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__":