Merge #68 Add support for sending messages
This commit is contained in:
commit
6f00f20b3d
20
bin/pungi-fedmsg-notification
Executable file
20
bin/pungi-fedmsg-notification
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import fedmsg
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def send(cmd, data):
|
||||
topic = 'compose.%s' % cmd.replace('-', '.').lower()
|
||||
fedmsg.publish(topic=topic, modname='pungi', msg=data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('cmd')
|
||||
|
||||
opts = parser.parse_args()
|
||||
data = json.load(sys.stdin)
|
||||
send(opts.cmd, data)
|
@ -26,6 +26,7 @@ locale.setlocale(locale.LC_ALL, "C")
|
||||
|
||||
|
||||
COMPOSE = None
|
||||
NOTIFIER = None
|
||||
|
||||
|
||||
def main():
|
||||
@ -187,8 +188,19 @@ def main():
|
||||
def run_compose(compose):
|
||||
import pungi.phases
|
||||
import pungi.metadata
|
||||
import pungi.notifier
|
||||
|
||||
errors = []
|
||||
|
||||
# initializer notifier
|
||||
compose.notifier = pungi.notifier.PungiNotifier(compose)
|
||||
try:
|
||||
compose.notifier.validate()
|
||||
except ValueError as ex:
|
||||
errors.extend(["NOTIFIER: %s" % m for m in ex.message.split('\n')])
|
||||
|
||||
compose.write_status("STARTED")
|
||||
compose.notifier.send('start')
|
||||
compose.log_info("Host: %s" % socket.gethostname())
|
||||
compose.log_info("User name: %s" % getpass.getuser())
|
||||
compose.log_info("Working directory: %s" % os.getcwd())
|
||||
@ -216,7 +228,6 @@ def run_compose(compose):
|
||||
test_phase = pungi.phases.TestPhase(compose)
|
||||
|
||||
# check if all config options are set
|
||||
errors = []
|
||||
for phase in (init_phase, pkgset_phase, createrepo_phase,
|
||||
buildinstall_phase, productimg_phase, gather_phase,
|
||||
extrafiles_phase, createiso_phase, liveimages_phase,
|
||||
@ -230,6 +241,7 @@ def run_compose(compose):
|
||||
errors.append("%s: %s" % (phase.name.upper(), i))
|
||||
if errors:
|
||||
for i in errors:
|
||||
compose.notifier.send('abort')
|
||||
compose.log_error(i)
|
||||
print(i)
|
||||
sys.exit(1)
|
||||
@ -319,6 +331,7 @@ def run_compose(compose):
|
||||
|
||||
compose.log_info("Compose finished: %s" % compose.topdir)
|
||||
compose.write_status("FINISHED")
|
||||
compose.notifier.send('finish')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@ -333,6 +346,8 @@ if __name__ == "__main__":
|
||||
COMPOSE.write_status("DOOMED")
|
||||
import kobo.tback
|
||||
open(tb_path, "w").write(kobo.tback.Traceback().get_traceback())
|
||||
if COMPOSE.notifier:
|
||||
COMPOSE.notifier.send('doomed')
|
||||
else:
|
||||
print("Exception: %s" % ex)
|
||||
raise
|
||||
|
@ -561,3 +561,39 @@ Example usage
|
||||
>>> from pungi.paths import translate_paths
|
||||
>>> print translate_paths(compose_object_with_mapping, "/mnt/a/c/somefile")
|
||||
http://b/dir/c/somefile
|
||||
|
||||
|
||||
Progress notification
|
||||
=====================
|
||||
|
||||
*Pungi* has the ability to emit notification messages about progress and
|
||||
status. These can be used to e.g. send messages to *fedmsg*. This is
|
||||
implemented by actually calling a separate script.
|
||||
|
||||
The script will be called with one argument describing action that just
|
||||
happened. A JSON-encoded object will be passed to standard input to provide
|
||||
more information about the event. At least, the object will contain a
|
||||
``compose_id`` key.
|
||||
|
||||
Currently these messages are sent:
|
||||
|
||||
* ``start`` -- when composing starts
|
||||
* ``abort`` -- when compose is aborted due to incorrect configuration
|
||||
* ``finish`` -- on successful finish of compose
|
||||
* ``doomed`` -- when an error happens
|
||||
* ``phase-start`` -- on start of a phase
|
||||
* ``phase-stop`` -- when phase is finished
|
||||
|
||||
For phase related messages ``phase_name`` key is provided as well.
|
||||
|
||||
The script is invoked in compose directory and can read other information
|
||||
there.
|
||||
|
||||
A ``pungi-fedmsg-notification`` script is provided and understands this
|
||||
interface.
|
||||
|
||||
Config options
|
||||
--------------
|
||||
|
||||
**notification_script**
|
||||
(*str*) -- executable to be invoked to send the message
|
||||
|
@ -102,6 +102,7 @@ class Compose(kobo.log.LoggingBase):
|
||||
self.just_phases = just_phases or []
|
||||
self.old_composes = old_composes or []
|
||||
self.koji_event = koji_event
|
||||
self.notifier = None
|
||||
|
||||
# intentionally upper-case (visible in the code)
|
||||
self.DEBUG = debug
|
||||
|
74
pungi/notifier.py
Normal file
74
pungi/notifier.py
Normal file
@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; version 2 of the License.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Library General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
|
||||
import json
|
||||
import threading
|
||||
|
||||
from kobo import shortcuts
|
||||
from pungi.checks import validate_options
|
||||
|
||||
|
||||
class PungiNotifier(object):
|
||||
"""Wrapper around an external script for sending messages.
|
||||
|
||||
If no script is configured, the messages are just silently ignored. If the
|
||||
script fails, a warning will be logged, but the compose process will not be
|
||||
interrupted.
|
||||
"""
|
||||
config_options = (
|
||||
{
|
||||
"name": "notification_script",
|
||||
"expected_types": [str],
|
||||
"optional": True
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self, compose):
|
||||
self.compose = compose
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def validate(self):
|
||||
errors = validate_options(self.compose.conf, self.config_options)
|
||||
if errors:
|
||||
raise ValueError("\n".join(errors))
|
||||
|
||||
def _update_args(self, data):
|
||||
"""Add compose related information to the data."""
|
||||
data.setdefault('compose_id', self.compose.compose_id)
|
||||
|
||||
def send(self, msg, **kwargs):
|
||||
"""Send a message.
|
||||
|
||||
The actual meaning of ``msg`` depends on what the notification script
|
||||
will be doing. The keyword arguments will be JSON-encoded and passed on
|
||||
to standard input of the notification process.
|
||||
|
||||
Unless you specify it manually, a ``compose_id`` key with appropriate
|
||||
value will be automatically added.
|
||||
"""
|
||||
script = self.compose.conf.get('notification_script', None)
|
||||
if not script:
|
||||
return
|
||||
|
||||
self._update_args(kwargs)
|
||||
|
||||
with self.lock:
|
||||
ret, _ = shortcuts.run((script, msg),
|
||||
stdin_data=json.dumps(kwargs),
|
||||
can_fail=True,
|
||||
workdir=self.compose.paths.compose.topdir(),
|
||||
return_stdout=False)
|
||||
if ret != 0:
|
||||
self.compose.log_warning('Failed to invoke notification script.')
|
@ -14,7 +14,6 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
|
||||
|
||||
from pungi.checks import validate_options
|
||||
|
||||
|
||||
@ -59,6 +58,7 @@ class PhaseBase(object):
|
||||
self.finished = True
|
||||
return
|
||||
self.compose.log_info("[BEGIN] %s" % self.msg)
|
||||
self.compose.notifier.send('phase-start', phase_name=self.name)
|
||||
self.run()
|
||||
|
||||
def stop(self):
|
||||
@ -68,6 +68,7 @@ class PhaseBase(object):
|
||||
self.pool.stop()
|
||||
self.finished = True
|
||||
self.compose.log_info("[DONE ] %s" % self.msg)
|
||||
self.compose.notifier.send('phase-stop', phase_name=self.name)
|
||||
|
||||
def run(self):
|
||||
raise NotImplementedError
|
||||
|
@ -54,6 +54,7 @@ class CreateisoPhase(PhaseBase):
|
||||
def run(self):
|
||||
iso = IsoWrapper(logger=self.compose._logger)
|
||||
symlink_isos_to = self.compose.conf.get("symlink_isos_to", None)
|
||||
deliverables = []
|
||||
|
||||
commands = []
|
||||
for variant in self.compose.get_variants(types=["variant", "layered-product", "optional"], recursive=True):
|
||||
@ -96,6 +97,7 @@ class CreateisoPhase(PhaseBase):
|
||||
self.compose.log_warning("Skipping mkisofs, image already exists: %s" % iso_path)
|
||||
continue
|
||||
iso_name = os.path.basename(iso_path)
|
||||
deliverables.append(iso_path)
|
||||
|
||||
graft_points = prepare_iso(self.compose, arch, variant, disc_num=disc_num, disc_count=disc_count, split_iso_data=iso_data)
|
||||
|
||||
@ -176,6 +178,8 @@ class CreateisoPhase(PhaseBase):
|
||||
cmd["cmd"] = " && ".join(cmd["cmd"])
|
||||
commands.append(cmd)
|
||||
|
||||
self.compose.notifier.send('createiso-targets', deliverables=deliverables)
|
||||
|
||||
for cmd in commands:
|
||||
self.pool.add(CreateIsoThread(self.pool))
|
||||
self.pool.queue_put((self.compose, cmd))
|
||||
@ -197,6 +201,10 @@ class CreateIsoThread(WorkerThread):
|
||||
# TODO: remove jigdo & template
|
||||
except OSError:
|
||||
pass
|
||||
compose.notifier.send('createiso-imagefail',
|
||||
file=cmd['iso_path'],
|
||||
arch=cmd['arch'],
|
||||
variant=str(cmd['variant']))
|
||||
|
||||
def process(self, item, num):
|
||||
compose, cmd = item
|
||||
@ -281,6 +289,10 @@ class CreateIsoThread(WorkerThread):
|
||||
# add: boot.iso
|
||||
|
||||
self.pool.log_info("[DONE ] %s" % msg)
|
||||
compose.notifier.send('createiso-imagedone',
|
||||
file=cmd['iso_path'],
|
||||
arch=cmd['arch'],
|
||||
variant=str(cmd['variant']))
|
||||
|
||||
|
||||
def split_iso(compose, arch, variant):
|
||||
|
1
setup.py
1
setup.py
@ -37,6 +37,7 @@ setup(
|
||||
'bin/pungi',
|
||||
'bin/pungi-koji',
|
||||
'bin/comps_filter',
|
||||
'bin/pungi-fedmsg-notifier',
|
||||
],
|
||||
data_files = [
|
||||
('/usr/share/pungi', glob.glob('share/*.xsl')),
|
||||
|
82
tests/test_notifier.py
Executable file
82
tests/test_notifier.py
Executable file
@ -0,0 +1,82 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import mock
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from pungi.notifier import PungiNotifier
|
||||
|
||||
|
||||
class TestNotifier(unittest.TestCase):
|
||||
def test_incorrect_config(self):
|
||||
compose = mock.Mock(
|
||||
conf={'notification_script': [1, 2]}
|
||||
)
|
||||
|
||||
n = PungiNotifier(compose)
|
||||
with self.assertRaises(ValueError) as err:
|
||||
n.validate()
|
||||
self.assertIn('notification_script', err.message)
|
||||
|
||||
@mock.patch('kobo.shortcuts.run')
|
||||
def test_invokes_script(self, run):
|
||||
compose = mock.Mock(
|
||||
compose_id='COMPOSE_ID',
|
||||
conf={'notification_script': 'run-notify'},
|
||||
paths=mock.Mock(
|
||||
compose=mock.Mock(
|
||||
topdir=mock.Mock(return_value='/a/b')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
run.return_value = (0, None)
|
||||
|
||||
n = PungiNotifier(compose)
|
||||
data = {'foo': 'bar', 'baz': 'quux'}
|
||||
n.send('cmd', **data)
|
||||
|
||||
data['compose_id'] = 'COMPOSE_ID'
|
||||
run.assert_called_once_with(('run-notify', 'cmd'),
|
||||
stdin_data=json.dumps(data),
|
||||
can_fail=True, return_stdout=False, workdir='/a/b')
|
||||
|
||||
@mock.patch('kobo.shortcuts.run')
|
||||
def test_does_not_run_without_config(self, run):
|
||||
compose = mock.Mock(conf={})
|
||||
|
||||
n = PungiNotifier(compose)
|
||||
n.send('cmd', foo='bar', baz='quux')
|
||||
self.assertFalse(run.called)
|
||||
|
||||
@mock.patch('kobo.shortcuts.run')
|
||||
def test_logs_warning_on_failure(self, run):
|
||||
compose = mock.Mock(
|
||||
compose_id='COMPOSE_ID',
|
||||
log_warning=mock.Mock(),
|
||||
conf={'notification_script': 'run-notify'},
|
||||
paths=mock.Mock(
|
||||
compose=mock.Mock(
|
||||
topdir=mock.Mock(return_value='/a/b')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
run.return_value = (1, None)
|
||||
|
||||
n = PungiNotifier(compose)
|
||||
n.send('cmd')
|
||||
|
||||
run.assert_called_once_with(('run-notify', 'cmd'),
|
||||
stdin_data=json.dumps({'compose_id': 'COMPOSE_ID'}),
|
||||
can_fail=True, return_stdout=False, workdir='/a/b')
|
||||
self.assertTrue(compose.log_warning.called)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
Reference in New Issue
Block a user