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:
parent
18bd37ff2c
commit
a21c8a555d
@ -126,6 +126,8 @@ def main():
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--notification-script",
|
"--notification-script",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
help="script for sending progress notification messages"
|
help="script for sending progress notification messages"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program; if not, see <https://gnu.org/licenses/>.
|
# along with this program; if not, see <https://gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import pungi.util
|
import pungi.util
|
||||||
@ -27,8 +29,8 @@ class PungiNotifier(object):
|
|||||||
script fails, a warning will be logged, but the compose process will not be
|
script fails, a warning will be logged, but the compose process will not be
|
||||||
interrupted.
|
interrupted.
|
||||||
"""
|
"""
|
||||||
def __init__(self, cmd):
|
def __init__(self, cmds):
|
||||||
self.cmd = cmd
|
self.cmds = cmds
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.compose = None
|
self.compose = None
|
||||||
|
|
||||||
@ -53,7 +55,7 @@ class PungiNotifier(object):
|
|||||||
Unless you specify it manually, a ``compose_id`` key with appropriate
|
Unless you specify it manually, a ``compose_id`` key with appropriate
|
||||||
value will be automatically added.
|
value will be automatically added.
|
||||||
"""
|
"""
|
||||||
if not self.cmd:
|
if not self.cmds:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._update_args(kwargs)
|
self._update_args(kwargs)
|
||||||
@ -62,14 +64,29 @@ class PungiNotifier(object):
|
|||||||
workdir = self.compose.paths.compose.topdir()
|
workdir = self.compose.paths.compose.topdir()
|
||||||
|
|
||||||
with self.lock:
|
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:
|
if self.compose:
|
||||||
self.compose.log_debug("Notification: %r %r, %r" % (
|
self.compose.log_warning('Failed to invoke notification script.')
|
||||||
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.')
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import mock
|
import mock
|
||||||
import os
|
import os
|
||||||
@ -12,91 +13,106 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|||||||
from pungi.notifier import PungiNotifier
|
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):
|
class TestNotifier(unittest.TestCase):
|
||||||
@mock.patch('pungi.util.translate_path')
|
|
||||||
@mock.patch('kobo.shortcuts.run')
|
def setUp(self):
|
||||||
def test_invokes_script(self, run, translate_path):
|
super(TestNotifier, self).setUp()
|
||||||
compose = mock.Mock(
|
self.logfile = '/logs/notifications/notification-2017-06-28_09-34-00.log'
|
||||||
|
self.compose = mock.Mock(
|
||||||
compose_id='COMPOSE_ID',
|
compose_id='COMPOSE_ID',
|
||||||
|
log_warning=mock.Mock(),
|
||||||
paths=mock.Mock(
|
paths=mock.Mock(
|
||||||
compose=mock.Mock(
|
compose=mock.Mock(
|
||||||
topdir=mock.Mock(return_value='/a/b')
|
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)
|
run.return_value = (0, None)
|
||||||
translate_path.side_effect = lambda compose, x: x
|
translate_path.side_effect = lambda compose, x: x
|
||||||
|
|
||||||
n = PungiNotifier('run-notify')
|
n = PungiNotifier(['run-notify'])
|
||||||
n.compose = compose
|
n.compose = self.compose
|
||||||
data = {'foo': 'bar', 'baz': 'quux'}
|
n.send('cmd', **self.data)
|
||||||
n.send('cmd', **data)
|
|
||||||
|
|
||||||
data['compose_id'] = 'COMPOSE_ID'
|
makedirs.assert_called_once_with('/logs/notifications')
|
||||||
data['location'] = '/a/b'
|
self.assertItemsEqual(run.call_args_list, [self._call('run-notify', 'cmd')])
|
||||||
run.assert_called_once_with(('run-notify', 'cmd'),
|
|
||||||
stdin_data=json.dumps(data),
|
@mock.patch('pungi.util.translate_path')
|
||||||
can_fail=True, return_stdout=False, workdir='/a/b')
|
@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')
|
@mock.patch('kobo.shortcuts.run')
|
||||||
def test_translates_path(self, run):
|
def test_translates_path(self, run, makedirs):
|
||||||
compose = mock.Mock(
|
self.compose.paths.compose.topdir.return_value = '/root/a/b'
|
||||||
compose_id='COMPOSE_ID',
|
self.compose.conf = {
|
||||||
paths=mock.Mock(
|
"translate_paths": [("/root/", "http://example.com/compose/")],
|
||||||
compose=mock.Mock(
|
}
|
||||||
topdir=mock.Mock(return_value='/root/a/b')
|
|
||||||
)
|
|
||||||
),
|
|
||||||
conf={
|
|
||||||
"translate_paths": [("/root/", "http://example.com/compose/")],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
run.return_value = (0, None)
|
run.return_value = (0, None)
|
||||||
|
|
||||||
n = PungiNotifier('run-notify')
|
n = PungiNotifier(['run-notify'])
|
||||||
n.compose = compose
|
n.compose = self.compose
|
||||||
data = {'foo': 'bar', 'baz': 'quux'}
|
n.send('cmd', **self.data)
|
||||||
n.send('cmd', **data)
|
|
||||||
|
|
||||||
data['compose_id'] = 'COMPOSE_ID'
|
self.assertItemsEqual(
|
||||||
data['location'] = 'http://example.com/compose/a/b'
|
run.call_args_list,
|
||||||
run.assert_called_once_with(('run-notify', 'cmd'),
|
[self._call('run-notify', 'cmd', location='http://example.com/compose/a/b')])
|
||||||
stdin_data=json.dumps(data),
|
|
||||||
can_fail=True, return_stdout=False,
|
|
||||||
workdir='/root/a/b')
|
|
||||||
|
|
||||||
@mock.patch('kobo.shortcuts.run')
|
@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 = PungiNotifier(None)
|
||||||
n.send('cmd', foo='bar', baz='quux')
|
n.send('cmd', foo='bar', baz='quux')
|
||||||
self.assertFalse(run.called)
|
self.assertFalse(run.called)
|
||||||
|
|
||||||
@mock.patch('pungi.util.translate_path')
|
@mock.patch('pungi.util.translate_path')
|
||||||
@mock.patch('kobo.shortcuts.run')
|
@mock.patch('kobo.shortcuts.run')
|
||||||
def test_logs_warning_on_failure(self, run, translate_path):
|
def test_logs_warning_on_failure(self, run, translate_path, makedirs):
|
||||||
compose = mock.Mock(
|
|
||||||
compose_id='COMPOSE_ID',
|
|
||||||
log_warning=mock.Mock(),
|
|
||||||
paths=mock.Mock(
|
|
||||||
compose=mock.Mock(
|
|
||||||
topdir=mock.Mock(return_value='/a/b')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
translate_path.side_effect = lambda compose, x: x
|
translate_path.side_effect = lambda compose, x: x
|
||||||
run.return_value = (1, None)
|
run.return_value = (1, None)
|
||||||
|
|
||||||
n = PungiNotifier('run-notify')
|
n = PungiNotifier(['run-notify'])
|
||||||
n.compose = compose
|
n.compose = self.compose
|
||||||
n.send('cmd')
|
n.send('cmd', **self.data)
|
||||||
|
|
||||||
run.assert_called_once_with(('run-notify', 'cmd'),
|
self.assertItemsEqual(run.call_args_list, [self._call('run-notify', 'cmd')])
|
||||||
stdin_data=json.dumps({'compose_id': 'COMPOSE_ID', 'location': '/a/b'}),
|
self.assertTrue(self.compose.log_warning.called)
|
||||||
can_fail=True, return_stdout=False, workdir='/a/b')
|
|
||||||
self.assertTrue(compose.log_warning.called)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
Loading…
Reference in New Issue
Block a user