#576 task notification plugin
Closed 3 years ago by julian8628. Opened 6 years ago by julian8628.
julian8628/koji maven-notification  into  master

@@ -0,0 +1,115 @@ 

+ import smtplib

+ import sys

+ 

+ import koji

+ import koji.tasks as tasks

+ 

+ __all__ = ('TaskNotificationTask',)

+ 

+ class TaskNotificationTask(tasks.BaseTaskHandler):

+     Methods = ['taskNotification']

+ 

+     _taskWeight = 0.1

+ 

+     # XXX externalize these templates somewhere

+     subject_templ = """Task: #%(id)d Status: %(state_str)s Owner: %(owner_name)s"""

+     message_templ = \

+ """From: %(from_addr)s\r

+ Subject: %(subject)s\r

+ To: %(to_addrs)s\r

+ X-Koji-Task: %(id)s\r

+ X-Koji-Owner: %(owner_name)s\r

+ X-Koji-Status: %(state_str)s\r

+ X-Koji-Parent: %(parent)s\r

+ X-Koji-Method: %(method)s\r

+ \r

+ Task: %(id)s\r

+ Status: %(state_str)s\r

+ Owner: %(owner_name)s\r

+ Host: %(host_name)s\r

+ Method: %(method)s\r

+ Parent: %(parent)s\r

+ Arch: %(arch)s\r

+ Label: %(label)s\r

+ Created: %(create_time)s\r

+ Started: %(start_time)s\r

+ Finished: %(completion_time)s\r

+ %(failure)s\r

+ Task Info: %(weburl)s/taskinfo?taskID=%(id)i\r

+ """

+ 

+     def _get_notification_info(self, task_id, recipient, weburl):

+         taskinfo = self.session.getTaskInfo(task_id, request=True)

+ 

+         if taskinfo['host_id']:

+             hostinfo = self.session.getHost(taskinfo['host_id'])

+         else:

+             hostinfo = None

+ 

+         if taskinfo['owner']:

+             userinfo = self.session.getUser(taskinfo['owner'], strict=True)

+             taskinfo['owner_name'] = userinfo['name']

+         else:

+             taskinfo['owner_name'] = None

+ 

+         result = None

+         try:

+             result = self.session.getTaskResult(task_id)

+         except:

+             excClass, result = sys.exc_info()[:2]

+             if hasattr(result, 'faultString'):

+                 result = result.faultString

+             else:

+                 result = '%s: %s' % (excClass.__name__, result)

+             result = result.strip()

+             # clear the exception, since we're just using

+             # it for display purposes

+             sys.exc_clear()

+         if not result:

+             result = 'Unknown'

+         taskinfo['result'] = result

+ 

+         noti_info = taskinfo.copy()

+         noti_info['create_time'] = koji.formatTimeLong(taskinfo.get('create_time'))

+         noti_info['start_time'] = koji.formatTimeLong(taskinfo.get('start_time'))

+         noti_info['completion_time'] = koji.formatTimeLong(taskinfo.get('completion_time'))

+         noti_info['host_name'] = hostinfo and hostinfo['name'] or None

+         noti_info['state_str'] = koji.TASK_STATES[taskinfo['state']]

+ 

+         cancel_info = ''

+         failure_info = ''

+         if taskinfo['state'] == koji.TASK_STATES['CANCELED']:

+             # The owner of the buildNotification task is the one

+             # who canceled the task, it turns out.

+             this_task = self.session.getTaskInfo(self.id)

+             if this_task['owner']:

+                 canceler = self.session.getUser(this_task['owner'], strict=True)

+                 cancel_info = "\r\nCanceled by: %s\r\n" % canceler['name']

+         elif taskinfo['state'] == koji.TASK_STATES['FAILED']:

+             failure_data = taskinfo['result']

+             failed_host = '%s (%s)' % (noti_info['host_name'], noti_info['arch'])

+             failure_info = "\r\nTask#%s failed on %s:\r\n  %s" % (task_id, failed_host, failure_data)

+ 

+         noti_info['failure'] = failure_info or cancel_info or '\r\n'

+ 

+         noti_info['from_addr'] = self.options.from_addr

+         noti_info['to_addrs'] = recipient

+         noti_info['subject'] = self.subject_templ % noti_info

+         noti_info['weburl'] = weburl

+         return noti_info

+ 

+     def handler(self, recipient, task_id, weburl):

+         noti_info = self._get_notification_info(task_id, recipient, weburl)

+ 

+         message = self.message_templ % noti_info

+         # ensure message is in UTF-8

+         message = koji.fixEncoding(message)

+ 

+         server = smtplib.SMTP(self.options.smtphost)

+         # server.set_debuglevel(True)

+         self.logger.debug("send notification for task #%i to %s, message:\n%s" % (task_id, recipient, message))

+         server.sendmail(noti_info['from_addr'], recipient, message)

+         server.quit()

+ 

+         return 'sent notification of task #%i to: %s' % (task_id, recipient)

+ 

@@ -0,0 +1,19 @@ 

+ # config file for the Koji task_notification plugin

+ 

+ [filters]

+ # Unix shell style patterns of task methods to indicate whose notification

+ # can be triggered

+ # Multiple values are delimited by comma or <space>

+ # Default value is '*'

+ methods = maven,runroot,distRepo

+ 

+ # Unix shell style patterns of task methods to indicate whose notification

+ # is forbidden

+ # Multiple values are delimited by comma or <space>

I would add here, that defining disallowed_methods never override *Notification - it will be added anyway.

+ # disallowed_methods = *Notification

+ 

+ # Unix shell style patterns of task states to indicate whose notification

+ # can be triggered

+ # Multiple values are delimited by comma or <space>

+ # Default value is '*'

+ states = FAILED,CANCELED

@@ -0,0 +1,76 @@ 

+ # koji hub plugin to trigger task notification.

+ # by default, only failed MavenTask can trigger a TaskNotification,

+ # which is exported as a task handler in a relative koji builder plugin.

+ 

+ 

+ import ConfigParser

+ import re

+ import sys

+ 

+ import six

+ 

+ import koji

+ from koji.context import context

+ from koji.plugin import callback, ignore_error

+ from koji.util import multi_fnmatch

+ 

+ # XXX - have to import kojihub for make_task

+ sys.path.insert(0, '/usr/share/koji-hub/')

+ import kojihub

+ 

+ __all__ = ('task_notification',)

+ 

+ CONFIG_FILE = '/etc/koji-hub/plugins/task_notification.conf'

+ FILTERS = {'methods': (['*'], None),

+            'disallowed_methods': ([], ['*Notification']),

+            'states': (['*'], None)}

+ 

+ 

+ def read_config():

+     result = {}

+     config = ConfigParser.SafeConfigParser()

+     config.read(CONFIG_FILE)

+     for k, (default, force) in six.iteritems(FILTERS):

+         try:

+             value = config.get('filters', k)

+             value = re.split(r'[\s,]+', value)

+         except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):

+             value = default

+         if force is not None:

+             value.extend(force)

+         result[k] = value

+     return result

+ 

+ 

+ def task_notification(task_id):

+     """Trigger a notification of a task to the owner via email"""

+     if context.opts.get('DisableNotifications'):

+         return

+     # sanity check for the existence of task

+     taskinfo = kojihub.Task(task_id).getInfo(strict=True)

+     # only send notification to the task's owner

+     user = kojihub.get_user(taskinfo['owner'], strict=True)

+     if user['status'] == koji.USER_STATUS['BLOCKED']:

+         raise koji.GenericError('Unable to send notification to disabled'

+                                 ' task#%s owner: %s' % (task_id, user['name']))

+     if user['usertype'] == koji.USERTYPES['HOST']:

+         raise koji.GenericError('Unable to send notification to host: %s whom'

+                                 ' owns task#%s' % (user['name'], task_id))

+     email_domain = context.opts['EmailDomain']

+     recipient = '%s@%s' % (user['name'], email_domain)

+     web_url = context.opts.get('KojiWebURL', 'http://localhost/koji')

+     kojihub.make_task("taskNotification", [recipient, task_id, web_url])

+ 

+ 

+ @callback('postTaskStateChange')

+ @ignore_error

+ def task_notification_callback(cbtype, *args, **kws):

+     if kws['attribute'] != 'state':

+         return

+     cfg = read_config()

Can you create some global CONFIG outside of this function? Every call to this function will open/parse that file, which is not effective, neither expected (I would expect, that any change to any config will be reflected only after kojid restart)

+     taskinfo = kws['info']

+     new = kws['new']

+     if multi_fnmatch(new, cfg['states']) \

+             and multi_fnmatch(taskinfo['method'], cfg['methods']) \

+             and not multi_fnmatch(taskinfo['method'], cfg['disallowed_methods']):

+         task_notification(taskinfo['id'])

@@ -0,0 +1,26 @@ 

+ import six

+ import copy

+ 

+ 

+ class FakeConfigParser(object):

+ 

+     def __init__(self, config):

+             self.CONFIG = copy.deepcopy(config)

+ 

+     def read(self, path):

+         return

+ 

+     def sections(self):

+         return self.CONFIG.keys()

+ 

+     def has_option(self, section, key):

+         return section in self.CONFIG and key in self.CONFIG[section]

+ 

+     def has_section(self, section):

+         return section in self.CONFIG

+ 

+     def get(self, section, key):

+         try:

+             return self.CONFIG[section][key]

+         except KeyError:

+             raise six.moves.configparser.NoOptionError(section, key)

@@ -1,17 +1,21 @@ 

  from __future__ import absolute_import

+ 

  import copy

  import unittest

+ 

+ import __main__

  import mock

- import six.moves.configparser

  

  # inject builder data

  from tests.test_builder.loadkojid import kojid

- import __main__

+ 

  __main__.BuildRoot = kojid.BuildRoot

  

- import koji

  import runroot

  

+ import koji

+ from .helper import FakeConfigParser

+ 

  

  CONFIG1 = {

          'paths': {
@@ -57,37 +61,10 @@ 

          }}

  

  

- class FakeConfigParser(object):

- 

-     def __init__(self, config=None):

-         if config is None:

-             self.CONFIG = copy.deepcopy(CONFIG1)

-         else:

-             self.CONFIG = copy.deepcopy(config)

- 

-     def read(self, path):

-         return

- 

-     def sections(self):

-         return self.CONFIG.keys()

- 

-     def has_option(self, section, key):

-         return section in self.CONFIG and key in self.CONFIG[section]

- 

-     def has_section(self, section):

-         return section in self.CONFIG

- 

-     def get(self, section, key):

-         try:

-             return self.CONFIG[section][key]

-         except KeyError:

-             raise six.moves.configparser.NoOptionError(section, key)

- 

- 

  class TestRunrootConfig(unittest.TestCase):

      @mock.patch('ConfigParser.SafeConfigParser')

      def test_bad_config_paths0(self, safe_config_parser):

-         cp = FakeConfigParser()

+         cp = FakeConfigParser(CONFIG1)

          del cp.CONFIG['path0']['mountpoint']

          safe_config_parser.return_value = cp

          session = mock.MagicMock()
@@ -100,7 +77,7 @@ 

  

      @mock.patch('ConfigParser.SafeConfigParser')

      def test_bad_config_absolute_path(self, safe_config_parser):

-         cp = FakeConfigParser()

+         cp = FakeConfigParser(CONFIG1)

          cp.CONFIG['paths']['default_mounts'] = ''

          safe_config_parser.return_value = cp

          session = mock.MagicMock()
@@ -113,7 +90,7 @@ 

  

      @mock.patch('ConfigParser.SafeConfigParser')

      def test_valid_config(self, safe_config_parser):

-         safe_config_parser.return_value = FakeConfigParser()

+         safe_config_parser.return_value = FakeConfigParser(CONFIG1)

          session = mock.MagicMock()

          options = mock.MagicMock()

          options.workdir = '/tmp/nonexistentdirectory'
@@ -163,7 +140,7 @@ 

  class TestMounts(unittest.TestCase):

      @mock.patch('ConfigParser.SafeConfigParser')

      def setUp(self, safe_config_parser):

-         safe_config_parser.return_value = FakeConfigParser()

+         safe_config_parser.return_value = FakeConfigParser(CONFIG1)

          self.session = mock.MagicMock()

          options = mock.MagicMock()

          options.workdir = '/tmp/nonexistentdirectory'

@@ -0,0 +1,291 @@ 

+ from __future__ import absolute_import

+ import os

+ import mock

+ import unittest

+ import xmlrpclib

+ from mock import call

+ 

+ import koji

+ from . import load_plugin

+ task_notification = load_plugin.load_plugin('builder', 'task_notification')

+ 

+ taskinfo = {'id': 111,

+             'host_id': 2,

+             'owner': 222,

+             'state': 0,

+             'parent': None,

+             'method': 'someMethod',

+             'arch': 'someArch',

+             'label': None,

+             'create_time': '2017-01-01 00:00:00.121313',

+             'start_time': '2017-02-01 00:00:00.121313',

+             'completion_time': '2017-01-01 00:00:00.121313'}

+ 

+ hostinfo = {'id': 2, 'name': 'task.host.com'}

+ userinfo = {'id': 222, 'name': 'somebody'}

+ taskresult = 'task result'

+ 

+ 

+ class TestTaskNotification(unittest.TestCase):

+     def setUp(self):

+         self.original_timezone = os.environ.get('TZ')

+         os.environ['TZ'] = 'US/Eastern'

+         self.session = mock.MagicMock()

+         self.session.getTaskInfo.return_value = taskinfo

+         self.session.getHost.return_value = hostinfo

+         self.session.getUser.return_value = userinfo

+         self.session.getTaskResult.return_value = taskresult

+         self.smtpClass = mock.patch("smtplib.SMTP").start()

+         self.smtp_server = self.smtpClass.return_value

+         options = mock.MagicMock()

+         options.from_addr = 'koji@example.com'

+         self.task = task_notification.TaskNotificationTask(666, 'taskNotification', {}, self.session, options)

+ 

+     def tearDown(self):

+         if self.original_timezone is None:

+             del os.environ['TZ']

+         else:

+             os.environ['TZ'] = self.original_timezone

+         mock.patch.stopall()

+ 

+     def reset_mock(self):

+         self.session.reset_mock()

+         self.smtpClass.reset_mock()

+ 

+     def test_task_notification_canceled(self):

+         ti = taskinfo.copy()

+         ti['state'] = 3  # canceled

+         self.session.getTaskInfo.side_effect = [ti,

+                                                 {'id': 666,

+                                                  'owner': 333}]

+         self.session.getUser.side_effect = [userinfo, {'id': 666, 'name': 'notitaskowner'}]

+ 

+         rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com')

+         self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com')

+         self.assertEqual(self.session.getTaskInfo.mock_calls, [call(111, request=True), call(666)])

+         self.session.getHost.assert_called_once_with(2)

+         self.assertEqual(self.session.getUser.mock_calls, [call(222, strict=True), call(333, strict=True)])

+         self.session.getTaskResult.assert_called_once_with(111)

+         self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com',

+                                                           'From: koji@example.com\r\n'

+                                                           'Subject: Task: #111 Status: CANCELED Owner: somebody\r\n'

+                                                           'To: someone@example.com\r\n'

+                                                           'X-Koji-Task: 111\r\n'

+                                                           'X-Koji-Owner: somebody\r\n'

+                                                           'X-Koji-Status: CANCELED\r\n'

+                                                           'X-Koji-Parent: None\r\n'

+                                                           'X-Koji-Method: someMethod\r\n\r\n'

+                                                           'Task: 111\r\n'

+                                                           'Status: CANCELED\r\n'

+                                                           'Owner: somebody\r\n'

+                                                           'Host: task.host.com\r\n'

+                                                           'Method: someMethod\r\n'

+                                                           'Parent: None\r\n'

+                                                           'Arch: someArch\r\n'

+                                                           'Label: None\r\n'

+                                                           'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n'

+                                                           'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n'

+                                                           'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n'

+                                                           'Canceled by: notitaskowner\r\n\r\n'

+                                                           'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n')

+ 

+     def test_task_notification_failed(self):

+         ti = taskinfo.copy()

+         ti['state'] = 5  # failed

+         self.session.getTaskInfo.return_value = ti

+ 

+         rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com')

+         self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com')

+         self.session.getTaskInfo.assert_called_once_with(111, request=True)

+         self.session.getHost.assert_called_once_with(2)

+         self.session.getUser.assert_called_once_with(222, strict=True)

+         self.session.getTaskResult.assert_called_once_with(111)

+         self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com',

+                                                           'From: koji@example.com\r\n'

+                                                           'Subject: Task: #111 Status: FAILED Owner: somebody\r\n'

+                                                           'To: someone@example.com\r\n'

+                                                           'X-Koji-Task: 111\r\n'

+                                                           'X-Koji-Owner: somebody\r\n'

+                                                           'X-Koji-Status: FAILED\r\n'

+                                                           'X-Koji-Parent: None\r\n'

+                                                           'X-Koji-Method: someMethod\r\n\r\n'

+                                                           'Task: 111\r\n'

+                                                           'Status: FAILED\r\n'

+                                                           'Owner: somebody\r\n'

+                                                           'Host: task.host.com\r\n'

+                                                           'Method: someMethod\r\n'

+                                                           'Parent: None\r\n'

+                                                           'Arch: someArch\r\n'

+                                                           'Label: None\r\n'

+                                                           'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n'

+                                                           'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n'

+                                                           'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n'

+                                                           'Task#111 failed on task.host.com (someArch):\r\n'

+                                                           '  task result\r\n'

+                                                           'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n')

+ 

+         self.reset_mock()

+         self.session.getTaskResult.return_value = None

+         rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com')

+         self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com')

+         self.session.getTaskInfo.assert_called_once_with(111, request=True)

+         self.session.getHost.assert_called_once_with(2)

+         self.session.getUser.assert_called_once_with(222, strict=True)

+         self.session.getTaskResult.assert_called_once_with(111)

+         self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com',

+                                                           'From: koji@example.com\r\n'

+                                                           'Subject: Task: #111 Status: FAILED Owner: somebody\r\n'

+                                                           'To: someone@example.com\r\n'

+                                                           'X-Koji-Task: 111\r\n'

+                                                           'X-Koji-Owner: somebody\r\n'

+                                                           'X-Koji-Status: FAILED\r\n'

+                                                           'X-Koji-Parent: None\r\n'

+                                                           'X-Koji-Method: someMethod\r\n\r\n'

+                                                           'Task: 111\r\n'

+                                                           'Status: FAILED\r\n'

+                                                           'Owner: somebody\r\n'

+                                                           'Host: task.host.com\r\n'

+                                                           'Method: someMethod\r\n'

+                                                           'Parent: None\r\n'

+                                                           'Arch: someArch\r\n'

+                                                           'Label: None\r\n'

+                                                           'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n'

+                                                           'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n'

+                                                           'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n'

+                                                           'Task#111 failed on task.host.com (someArch):\r\n'

+                                                           '  Unknown\r\n'

+                                                           'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n')

+ 

+         self.reset_mock()

+         self.session.getTaskResult.side_effect = xmlrpclib.Fault(1231, 'xmlrpc fault')

+         rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com')

+         self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com')

+         self.session.getTaskInfo.assert_called_once_with(111, request=True)

+         self.session.getHost.assert_called_once_with(2)

+         self.session.getUser.assert_called_once_with(222, strict=True)

+         self.session.getTaskResult.assert_called_once_with(111)

+         self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com',

+                                                           'From: koji@example.com\r\n'

+                                                           'Subject: Task: #111 Status: FAILED Owner: somebody\r\n'

+                                                           'To: someone@example.com\r\n'

+                                                           'X-Koji-Task: 111\r\n'

+                                                           'X-Koji-Owner: somebody\r\n'

+                                                           'X-Koji-Status: FAILED\r\n'

+                                                           'X-Koji-Parent: None\r\n'

+                                                           'X-Koji-Method: someMethod\r\n\r\n'

+                                                           'Task: 111\r\n'

+                                                           'Status: FAILED\r\n'

+                                                           'Owner: somebody\r\n'

+                                                           'Host: task.host.com\r\n'

+                                                           'Method: someMethod\r\n'

+                                                           'Parent: None\r\n'

+                                                           'Arch: someArch\r\n'

+                                                           'Label: None\r\n'

+                                                           'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n'

+                                                           'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n'

+                                                           'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n'

+                                                           'Task#111 failed on task.host.com (someArch):\r\n'

+                                                           '  xmlrpc fault\r\n'

+                                                           'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n')

+ 

+         self.reset_mock()

+         self.session.getTaskResult.side_effect = koji.GenericError('koji generic error')

+         rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com')

+         self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com')

+         self.session.getTaskInfo.assert_called_once_with(111, request=True)

+         self.session.getHost.assert_called_once_with(2)

+         self.session.getUser.assert_called_once_with(222, strict=True)

+         self.session.getTaskResult.assert_called_once_with(111)

+         self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com',

+                                                           'From: koji@example.com\r\n'

+                                                           'Subject: Task: #111 Status: FAILED Owner: somebody\r\n'

+                                                           'To: someone@example.com\r\n'

+                                                           'X-Koji-Task: 111\r\n'

+                                                           'X-Koji-Owner: somebody\r\n'

+                                                           'X-Koji-Status: FAILED\r\n'

+                                                           'X-Koji-Parent: None\r\n'

+                                                           'X-Koji-Method: someMethod\r\n\r\n'

+                                                           'Task: 111\r\n'

+                                                           'Status: FAILED\r\n'

+                                                           'Owner: somebody\r\n'

+                                                           'Host: task.host.com\r\n'

+                                                           'Method: someMethod\r\n'

+                                                           'Parent: None\r\n'

+                                                           'Arch: someArch\r\n'

+                                                           'Label: None\r\n'

+                                                           'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n'

+                                                           'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n'

+                                                           'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n'

+                                                           'Task#111 failed on task.host.com (someArch):\r\n'

+                                                           '  GenericError: koji generic error\r\n'

+                                                           'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n')

+ 

+     def test_task_notification_other_status(self):

+         ti = taskinfo.copy()

+         ti['state'] = 2  # closed

+         self.session.getTaskInfo.return_value = ti

+ 

+         rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com')

+         self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com')

+         self.session.getTaskInfo.assert_called_once_with(111, request=True)

+         self.session.getHost.assert_called_once_with(2)

+         self.session.getUser.assert_called_once_with(222, strict=True)

+         self.session.getTaskResult.assert_called_once_with(111)

+         self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com',

+                                                           'From: koji@example.com\r\n'

+                                                           'Subject: Task: #111 Status: CLOSED Owner: somebody\r\n'

+                                                           'To: someone@example.com\r\n'

+                                                           'X-Koji-Task: 111\r\n'

+                                                           'X-Koji-Owner: somebody\r\n'

+                                                           'X-Koji-Status: CLOSED\r\n'

+                                                           'X-Koji-Parent: None\r\n'

+                                                           'X-Koji-Method: someMethod\r\n\r\n'

+                                                           'Task: 111\r\n'

+                                                           'Status: CLOSED\r\n'

+                                                           'Owner: somebody\r\n'

+                                                           'Host: task.host.com\r\n'

+                                                           'Method: someMethod\r\n'

+                                                           'Parent: None\r\n'

+                                                           'Arch: someArch\r\n'

+                                                           'Label: None\r\n'

+                                                           'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n'

+                                                           'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n'

+                                                           'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n\r\n'

+                                                           'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n')

+ 

+     def test_task_notification_no_host_user(self):

+         ti = taskinfo.copy()

+         ti['state'] = 2  # closed

+         ti['host_id'] = None

+         ti['owner'] = None

+         self.session.getTaskInfo.return_value = ti

+         self.session.getHost.return_value = None

+         self.session.getUser.return_value = None

+ 

+         rv = self.task.handler('someone@example.com', 111, 'https://kojiurl.com')

+         self.assertEqual(rv, 'sent notification of task #111 to: someone@example.com')

+         self.session.getTaskInfo.assert_called_once_with(111, request=True)

+         self.session.getHost.assert_not_called()

+         self.session.getUser.assert_not_called()

+         self.session.getTaskResult.assert_called_once_with(111)

+         self.smtp_server.sendmail.assert_called_once_with('koji@example.com', 'someone@example.com',

+                                                           'From: koji@example.com\r\n'

+                                                           'Subject: Task: #111 Status: CLOSED Owner: None\r\n'

+                                                           'To: someone@example.com\r\n'

+                                                           'X-Koji-Task: 111\r\n'

+                                                           'X-Koji-Owner: None\r\n'

+                                                           'X-Koji-Status: CLOSED\r\n'

+                                                           'X-Koji-Parent: None\r\n'

+                                                           'X-Koji-Method: someMethod\r\n\r\n'

+                                                           'Task: 111\r\n'

+                                                           'Status: CLOSED\r\n'

+                                                           'Owner: None\r\n'

+                                                           'Host: None\r\n'

+                                                           'Method: someMethod\r\n'

+                                                           'Parent: None\r\n'

+                                                           'Arch: someArch\r\n'

+                                                           'Label: None\r\n'

+                                                           'Created: Sun, 01 Jan 2017 00:00:00 EST\r\n'

+                                                           'Started: Wed, 01 Feb 2017 00:00:00 EST\r\n'

+                                                           'Finished: Sun, 01 Jan 2017 00:00:00 EST\r\n\r\n\r\n'

+                                                           'Task Info: https://kojiurl.com/taskinfo?taskID=111\r\n')

@@ -0,0 +1,180 @@ 

+ import unittest

+ 

+ import mock

+ 

+ from koji import GenericError

+ from koji.context import context

+ from . import load_plugin

+ from .helper import FakeConfigParser

+ 

+ task_notification = load_plugin.load_plugin('hub', 'task_notification')

+ 

+ CONFIG1 = {'filters': {

+     'methods': "someMethod,someotherMethod",

+     'disallowed_methods': 'disallowedMethods,*Whatever',

+     'states': "FAILED,NEW"

+ }}

+ 

+ CONFIG2 = {'filters': {

+     'methods': "*",

+     'states': "*"

+ }}

+ 

+ 

+ class TestTaskNotificationCallback(unittest.TestCase):

+     def setUp(self):

+         context.session = mock.MagicMock()

+         context.opts = {'DisableNotifications': False,

+                         'EmailDomain': 'example.com',

+                         'KojiWebURL': 'https://koji.org'}

+         self.parser = mock.patch('ConfigParser.SafeConfigParser',

+                                  return_value=FakeConfigParser(

+                                      CONFIG1)).start()

+         self.getTaskInfo = mock.patch('kojihub.Task.getInfo').start()

+         self.get_user = mock.patch('kojihub.get_user',

+                                    return_value={'id': 999, 'name': 'someone',

+                                                  'status': 0,

+                                                  'usertype': 0}).start()

+         self.make_task = mock.patch('kojihub.make_task').start()

+ 

+     def tearDown(self):

+         mock.patch.stopall()

+         task_notification.config = None

+ 

+     def test_basic_invocation(self):

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='FAILED',

+             info={'id': 123, 'method': 'someMethod'},

+         )

+         self.make_task.assert_called_once_with(

+             'taskNotification',

+             ['someone@example.com', 123, 'https://koji.org'])

+ 

+     def test_disable_notifications(self):

+         context.opts['DisableNotifications'] = True

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='FAILED',

+             info={'id': 123, 'method': 'someMethod'}

+         )

+         self.make_task.assert_not_called()

+ 

+     def test_not_state_change(self):

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='others',

+             new='something',

+             info={'id': 123, 'method': 'someMethod'}

+         )

+         self.make_task.assert_not_called()

+ 

+     def test_disallowed_state(self):

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='something',

+             info={'id': 123, 'method': 'someMethod'}

+         )

+         self.make_task.assert_not_called()

+ 

+     def test_disallowed_method(self):

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='FAILED',

+             info={'id': 123, 'method': 'disallowedMethod'}

+         )

+         self.make_task.assert_not_called()

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='FAILED',

+             info={'id': 123, 'method': 'xxxMethod'}

+         )

+         self.make_task.assert_not_called()

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='FAILED',

+             info={'id': 123, 'method': 'Whatever'}

+         )

+         self.make_task.assert_not_called()

+ 

+     def test_method_self(self):

+         self.parser.return_value = FakeConfigParser(CONFIG2)

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='somestate',

+             info={'id': 123, 'method': 'taskNotification'}

+         )

+         self.make_task.assert_not_called()

+ 

+     def test_allow_everything(self):

+         self.parser.return_value = FakeConfigParser(CONFIG2)

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='somestate',

+             info={'id': 123, 'method': 'xxxMethod'}

+         )

+         self.make_task.assert_called_once_with(

+             'taskNotification',

+             ['someone@example.com', 123, 'https://koji.org'])

+ 

+     def test_force_disallowed(self):

+         self.parser.return_value = FakeConfigParser(CONFIG2)

+         task_notification.task_notification_callback(

+             'postTaskStateChange',

+             attribute='state',

+             new='somestate',

+             info={'id': 123, 'method': 'xxxNotification'}

+         )

+         self.make_task.assert_not_called()

+ 

+     def test_no_user_found(self):

+         self.get_user.side_effect = GenericError('not found')

+         with self.assertRaises(GenericError) as cm:

+             task_notification.task_notification_callback(

+                 'postTaskStateChange',

+                 attribute='state',

+                 new='FAILED',

+                 info={'id': 123, 'method': 'someMethod'}

+             )

+         self.assertEqual(cm.exception.args[0], 'not found')

+         self.make_task.assert_not_called()

+ 

+     def test_disabled_owner(self):

+         self.get_user.return_value = {'id': 999,

+                                       'name': 'someone',

+                                       'status': 1,

+                                       'usertype': 0}

+         with self.assertRaises(GenericError) as cm:

+             task_notification.task_notification_callback(

+                 'postTaskStateChange',

+                 attribute='state',

+                 new='FAILED',

+                 info={'id': 123, 'method': 'someMethod'}

+             )

+         self.assertEqual(cm.exception.args[0], 'Unable to send notification to'

+                                                ' disabled task#123 owner: someone')

+         self.make_task.assert_not_called()

+ 

+     def test_host_owner(self):

+         self.get_user.return_value = {'id': 999,

+                                       'name': 'somehost',

+                                       'status': 0,

+                                       'usertype': 1}

+         with self.assertRaises(GenericError) as cm:

+             task_notification.task_notification_callback(

+                 'postTaskStateChange',

+                 attribute='state',

+                 new='FAILED',

+                 info={'id': 123, 'method': 'someMethod'}

+             )

+         self.assertEqual(cm.exception.args[0], 'Unable to send notification to'

+                                                ' host: somehost whom owns task#123')

+         self.make_task.assert_not_called() 

\ No newline at end of file

3 new commits added

  • unit test for task_notification hub plugin
  • builder unit tests for task_notification plugin
  • Task Notification Plugin
6 years ago

rebased onto 8a2e08e199983ccf66860ab2a6546e115836d3e5

6 years ago

I'm not sure if the words 'allowed' and 'permissions' are the right terminology here. Perhaps the config could look more like:

[filters]
methods = maven,runroot,distRepo
states = FAILED,CANCELED

Also, probably need to be more friendly and accept , and or <space> as a separator in the config

For the if allowed_methods == '*' check. I wonder if we shouldn't just use multi_fnmatch.

The disallowed_methods global seems like it is supposed to be complementary to the allowed_methods one, but it isn't configurable like the other.

I think we might want to hard-code the exclusion of taskNotification. Also tagNotification and buildNotification.

I don't know if we need that sanity check for the existence of the task. We're handling callback -- I think we can trust the data.

For recipients, we should probably exclude hosts and disabled users.

We can pick this up again for 1.17

rebased onto a6e0ce5

5 years ago

I would add here, that defining disallowed_methods never override *Notification - it will be added anyway.

Can you create some global CONFIG outside of this function? Every call to this function will open/parse that file, which is not effective, neither expected (I would expect, that any change to any config will be reflected only after kojid restart)

Some simple description should be also added to docs/source/plugins.rst

Pull-Request has been closed by julian8628

3 years ago