notification: Allow specifying multiple scripts

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ář <lsedlar@redhat.com>
This commit is contained in:
Lubomír Sedlář 2017-06-28 10:26:29 +02:00
parent 18bd37ff2c
commit a21c8a555d
3 changed files with 102 additions and 67 deletions

View File

@ -126,6 +126,8 @@ def main():
)
parser.add_argument(
"--notification-script",
action="append",
default=[],
help="script for sending progress notification messages"
)
parser.add_argument(

View File

@ -12,7 +12,9 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://gnu.org/licenses/>.
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.')

View File

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