diff --git a/master/buildbot/reporters/mail.py b/master/buildbot/reporters/mail.py index 5d8eb7ee0a3..028ebda990b 100644 --- a/master/buildbot/reporters/mail.py +++ b/master/buildbot/reporters/mail.py @@ -242,7 +242,8 @@ def buildsetComplete(self, key, msg): self.master, bsid, wantProperties=self.messageFormatter.wantProperties, wantSteps=self.messageFormatter.wantSteps, - wantPreviousBuild=self.wantPreviousBuild()) + wantPreviousBuild=self.wantPreviousBuild(), + wantLogs=self.messageFormatter.wantLogs) builds = res['builds'] buildset = res['buildset'] @@ -262,7 +263,8 @@ def buildComplete(self, key, build): self.master, buildset, [build], wantProperties=self.messageFormatter.wantProperties, wantSteps=self.messageFormatter.wantSteps, - wantPreviousBuild=self.wantPreviousBuild()) + wantPreviousBuild=self.wantPreviousBuild(), + wantLogs=self.messageFormatter.wantLogs) # only include builds for which isMailNeeded returns true if self.isMailNeeded(build): self.buildMessage( diff --git a/master/buildbot/reporters/message.py b/master/buildbot/reporters/message.py index 270a6850380..3b2197d7cf3 100644 --- a/master/buildbot/reporters/message.py +++ b/master/buildbot/reporters/message.py @@ -1,7 +1,23 @@ +# This file is part of Buildbot. Buildbot 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. +# +# 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 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., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright Buildbot Team Members + import os import jinja2 +from buildbot import config from buildbot.process.results import CANCELLED from buildbot.process.results import EXCEPTION from buildbot.process.results import FAILURE @@ -12,25 +28,54 @@ class MessageFormatter(object): - template_name = 'default_mail.txt' + template_filename = 'default_mail.txt' template_type = 'plain' - wantProperties = True - wantSteps = False - def __init__(self, template_name=None, template_dir=None, template_type=None): + def __init__(self, template_dir=None, + template_filename=None, template=None, template_name=None, + subject_filename=None, subject=None, + template_type=None, ctx=None, + wantProperties=True, wantSteps=False, wantLogs=False): - if template_dir is None: - template_dir = os.path.join(os.path.dirname(__file__), "templates") + if template_name is not None: + config.warnDeprecated('0.9.1', "template_name is deprecated, use template_filename") + template_filename = template_name - loader = jinja2.FileSystemLoader(template_dir) - self.env = jinja2.Environment( - loader=loader, undefined=jinja2.StrictUndefined) + self.body_template = self.getTemplate(template_filename, template_dir, template) + self.subject_template = None + if subject_filename or subject: + self.subject_template = self.getTemplate(subject_filename, template_dir, subject) - if template_name is not None: - self.template_name = template_name if template_type is not None: self.template_type = template_type + if ctx is None: + ctx = {} + + self.ctx = ctx + self.wantProperties = wantProperties + self.wantSteps = wantSteps + self.wantLogs = wantLogs + + def getTemplate(self, filename, dirname, content): + if content and (filename or dirname): + config.error("Only one of template or template path can be given") + + if content: + return jinja2.Template(content) + + if dirname is None: + dirname = os.path.join(os.path.dirname(__file__), "templates") + + loader = jinja2.FileSystemLoader(dirname) + env = jinja2.Environment( + loader=loader, undefined=jinja2.StrictUndefined) + + if filename is None: + filename = self.template_filename + + return env.get_template(filename) + def getDetectedStatus(self, mode, results, previous_results): if results == FAILURE: @@ -40,7 +85,7 @@ def getDetectedStatus(self, mode, results, previous_results): else: text = "failed build" elif results == WARNINGS: - text = "The Buildbot has detected a problem in the build" + text = "problem in the build" elif results == SUCCESS: if "change" in mode and previous_results is not None and previous_results != results: text = "restored build" @@ -109,13 +154,12 @@ def messageSummary(self, build, results): return text def __call__(self, mode, buildername, buildset, build, master, previous_results, blamelist): - """Generate a buildbot mail message and return a tuple of message text - and type.""" + """Generate a buildbot mail message and return a dictionary + containing the message body, type and subject.""" ss_list = buildset['sourcestamps'] results = build['results'] - tpl = self.env.get_template(self.template_name) - cxt = dict(results=build['results'], + ctx = dict(results=build['results'], mode=mode, buildername=buildername, workername=build['properties'].get( @@ -133,5 +177,9 @@ def __call__(self, mode, buildername, buildset, build, master, previous_results, summary=self.messageSummary(build, results), sourcestamps=self.messageSourceStamps(ss_list) ) - contents = tpl.render(cxt) - return {'body': contents, 'type': self.template_type} + ctx.update(self.ctx) + body = self.body_template.render(ctx) + email = {'body': body, 'type': self.template_type} + if self.subject_template is not None: + email['subject'] = self.subject_template.render(ctx) + return email diff --git a/master/buildbot/test/unit/test_reporters_message.py b/master/buildbot/test/unit/test_reporters_message.py index ac29d3bcc1b..e8b638e7bea 100644 --- a/master/buildbot/test/unit/test_reporters_message.py +++ b/master/buildbot/test/unit/test_reporters_message.py @@ -87,6 +87,20 @@ def test_message_success(self): Sincerely, -The Buildbot''')) + self.assertTrue('subject' not in res) + + @defer.inlineCallbacks + def test_inline_template(self): + self.message = message.MessageFormatter(template="URL: {{ build_url }} -- {{ summary }}") + res = yield self.doOneTest(SUCCESS, SUCCESS) + self.assertEqual(res['type'], "plain") + self.assertEqual(res['body'], "URL: http://localhost:8080/#builders/80/builds/1 -- Build succeeded!") + + @defer.inlineCallbacks + def test_inline_subject(self): + self.message = message.MessageFormatter(subject="subject") + res = yield self.doOneTest(SUCCESS, SUCCESS) + self.assertEqual(res['subject'], "subject") @defer.inlineCallbacks def test_message_failure(self): diff --git a/master/docs/manual/cfg-reporters.rst b/master/docs/manual/cfg-reporters.rst index 5e9d2550a60..070a0110700 100644 --- a/master/docs/manual/cfg-reporters.rst +++ b/master/docs/manual/cfg-reporters.rst @@ -109,137 +109,37 @@ If you want to require Transport Layer Security (TLS), then you can also set ``u If you see ``twisted.mail.smtp.TLSRequiredError`` exceptions in the log while using TLS, this can be due *either* to the server not supporting TLS or to a missing `PyOpenSSL`_ package on the BuildMaster system. In some cases it is desirable to have different information then what is provided in a standard MailNotifier message. -For this purpose MailNotifier provides the argument ``messageFormatter`` (a function) which allows for the creation of messages with unique content. +For this purpose MailNotifier provides the argument ``messageFormatter`` (an instance of ``MessageFormatter``) which allows for the creation of messages with unique content. For example, if only short emails are desired (e.g., for delivery to phones):: - from buildbot.plugins import reporters, util - def messageFormatter(mode, name, build, results, master_status): - result = util.Results[results] - - text = list() - text.append("STATUS: %s" % result.title()) - return { - 'body' : "\n".join(text), - 'type' : 'plain' - } - + from buildbot.plugins import reporters mn = reporters.MailNotifier(fromaddr="buildbot@example.org", sendToInterestedUsers=False, mode=('problem',), extraRecipients=['listaddr@example.org'], - messageFormatter=messageFormatter) - -Another example of a function delivering a customized html email containing the last 80 log lines of logs of the last build step is given below:: - - from future.utils import text_type - from buildbot.plugins import util, reporters - - import cgi, datetime - - # FIXME: this code is barely readable, we should provide a better example with use of jinja templates - # - def html_message_formatter(mode, name, build, results, master_status): - """Provide a customized message to Buildbot's MailNotifier. - - The last 80 lines of the log are provided as well as the changes - relevant to the build. Message content is formatted as html. - """ - result = util.Results[results] - - limit_lines = 80 - text = list() - text.append(u'

Build status: %s

' % result.upper()) - text.append(u'') - text.append(u"" % build.getWorkername()) - if master_status.getURLForThing(build): - text.append(u'' - % (master_status.getURLForThing(build), - master_status.getURLForThing(build)) - ) - text.append(u'' % build.getReason()) - source = u"" - for ss in build.getSourceStamps(): - if ss.codebase: - source += u'%s: ' % ss.codebase - if ss.branch: - source += u"[branch %s] " % ss.branch - if ss.revision: - source += ss.revision - else: - source += u"HEAD" - if ss.patch: - source += u" (plus patch)" - if ss.patch_info: # add patch comment - source += u" (%s)" % ss.patch_info[1] - text.append(u"" % source) - text.append(u"" % ",".join(build.getResponsibleUsers())) - text.append(u'
Worker for this Build:%s
Complete logs for all build steps:%s
Build Reason:%s
Build Source Stamp:%s
Blamelist:%s
') - if ss.changes: - text.append(u'

Recent Changes:

') - for c in ss.changes: - cd = c.asDict() - when = datetime.datetime.fromtimestamp(cd['when'] ).ctime() - text.append(u'') - text.append(u'' % cd['repository'] ) - text.append(u'' % cd['project'] ) - text.append(u'' % when) - text.append(u'' % cd['who'] ) - text.append(u'' % cd['comments'] ) - text.append(u'
Repository:%s
Project:%s
Time:%s
Changed by:%s
Comments:%s
') - files = cd['files'] - if files: - text.append(u'') - for file in files: - text.append(u'' % file['name'] ) - text.append(u'
Files
%s:
') - text.append(u'
') - # get all the steps in build in reversed order - rev_steps = reversed(build.getSteps()) - # find the last step that finished - for step in rev_steps: - if step.isFinished(): - break - # get logs for the last finished step - if step.isFinished(): - logs = step.getLogs() - # No step finished, loop just exhausted itself; so as a special case we fetch all logs - else: - logs = build.getLogs() - # logs within a step are in reverse order. Search back until we find stdio - for log in reversed(logs): - if log.getName() == 'stdio': - break - name = "%s.%s" % (log.getStep().getName(), log.getName()) - status, dummy = log.getStep().getResults() - # XXX logs no longer have getText methods!! - content = log.getText().splitlines() # Note: can be VERY LARGE - url = u'%s/steps/%s/logs/%s' % (master_status.getURLForThing(build), - log.getStep().getName(), - log.getName()) - - text.append(u'Detailed log of last build step: %s' - % (url, url)) - text.append(u'
') - text.append(u'

Last %d lines of "%s"

' % (limit_lines, name)) - unilist = list() - for line in content[len(content)-limit_lines:]: - unilist.append(cgi.escape(text_type(line,'utf-8'))) - text.append(u'
')
-            text.extend(unilist)
-            text.append(u'
') - text.append(u'

') - text.append(u'-The Buildbot') - return { - 'body': u"\n".join(text), - 'type': 'html' - } + messageFormatter=reporters.MessageFormatter(template="STATUS: {{ summary }}")) + +Another example of a function delivering a customized html email is given below:: + + from buildbot.plugins import reporters + + template=u'''\ +

Build status: {{ summary }}

+

Worker used: {{ workername }}

+ {% for step in build['steps'] %} +

{{ step['name'] }}: {{ step['result'] }}

+ {% endfor %} +

-- The Buildbot

+ ''' mn = reporters.MailNotifier(fromaddr="buildbot@example.org", sendToInterestedUsers=False, mode=('failing',), extraRecipients=['listaddr@example.org'], - messageFormatter=html_message_formatter) + messageFormatter=reporters.MessageFormatter( + template=template, template_type='html', + wantProperties=True, wantSteps=True)) .. _PyOpenSSL: http://pyopenssl.sourceforge.net/ @@ -378,13 +278,9 @@ MailNotifier arguments Regardless of the setting of ``lookup``, ``MailNotifier`` will also send mail to addresses in the ``extraRecipients`` list. ``messageFormatter`` - This is a optional function that can be used to generate a custom mail message. - A :func:`messageFormatter` function takes the mail mode (``mode``), builder name (``name``), the build Data API results (``build``), the result code (``results``), and a reference to the BuildMaster object (``master``), which can then be used to create additional Data API calls. - It returns a dictionary. - The ``body`` key gives a string that is the complete text of the message. - The ``type`` key is the message type ('plain' or 'html'). - The 'html' type should be used when generating an HTML message. - The ``subject`` key is optional, but gives the subject for the email. + This is an optional instance of the ``reporters.MessageFormatter`` class that can be used to generate a custom mail message. + This class uses the Jinja2_ templating language to generate the body and optionally the subject of the mails. + Templates can either be given inline (as string), or read from the filesystem. ``extraHeaders`` (dictionary). @@ -392,46 +288,82 @@ MailNotifier arguments Both the keys and the values may be a `Interpolate` instance. -As a help to those writing :func:`messageFormatter` functions, the following table describes how to get some useful pieces of information from the various data objects: +MessageFormatter arguments +++++++++++++++++++++++++++ -Name of the builder that generated this event - ``name`` +The easiest way to use the ``messageFormatter`` parameter is to create a new instance of the ``reporters.MessageFormatter`` class. +The constructor to that class takes the following arguments: -Title of the BuildMaster - ``master.config.title`` +``template_dir`` + This is the directory that is used to look for the various templates. -MailNotifier mode - ``mode`` (a combination of ``change``, ``failing``, ``passing``, ``problem``, ``warnings``, ``exception``, ``all``) +``template_filename`` + This is the name of the file in the ``template_dir`` directory that will be used to generate the body of the mail. + It defaults to ``default_mail.txt``. + +``template`` + If this parameter is set, this parameter indicates the content of the template used to generate the body of the mail as string. -Builder result as a string +``template_type`` + This indicates the type of the generated template. + Use either 'plain' (the default) or 'html'. - :: +``subject_filename`` + This is the name of the file in the ``template_dir`` directory that contains the content of the subject of the mail. - from buildbot.plugins import util - result_str = util.Results[results] - # one of 'success', 'warnings', 'failure', 'skipped', or 'exception' +``subject`` + Alternatively, this is the content of the subject of the mail as string. + +``ctx`` + This is an extension of the standard context that will be given to the templates. + Use this to add content to the templates that is otherwise not available. + +``wantProperties`` + This parameter (defaults to True) will extend the content of the given ``build`` object with the Properties from the build. + +``wantSteps`` + This parameter (defaults to False) will extend the content of the given ``build`` object with information about the steps of the build. + Use it only when necessary as this increases the overhead in term of CPU and memory on the master. + +``wantLogs`` + This parameter (defaults to False) will extend the content of the steps of the given ``build`` object with the full Logs of each steps from the build. + This requires ``wantSteps`` to be True. + Use it only when mandatory as this increases the overhead in term of CPU and memory on the master greatly. + + +As a help to those writing Jinja2 templates the following table describes how to get some useful pieces of information from the various data objects: + +Name of the builder that generated this event + ``{{ buildername }}`` + +Title of the BuildMaster + ``{{ projects }}`` + +MailNotifier mode + ``{{ mode }}`` (a combination of ``change``, ``failing``, ``passing``, ``problem``, ``warnings``, ``exception``, ``all``) URL to build page - ``reporters.utils.getURLForBuild(master, build['buildid'])`` + ``{{ build_url }}`` URL to buildbot main page - ``master.config.buildbotURL`` + ``{{ buildbot_url }}`` Build text - ``build['state_string']`` + ``{{ build['state_string'] }}`` Mapping of property names to (values, source) - ``build['properties']`` + ``{{ build['properties'] }}`` -Worker name - ``build['properties']['workername']`` +For instance the build reason (from a forced build) + ``{{ build['properties']['reason'][0] }}`` -Build reason (from a forced build) - ``build['properties']['reason']`` +Worker name + ``{{ workername }}`` List of responsible users - ``reporters.utils.getResponsibleUsersForBuild(master, build['buildid'])`` + ``{{ blamelist | join(', ') }}`` +.. _Jinja2: http://jinja.pocoo.org/docs/dev/templates/ .. bb:reporter:: IRC diff --git a/master/docs/relnotes/index.rst b/master/docs/relnotes/index.rst index 41e634c22e7..aebb8e9309a 100644 --- a/master/docs/relnotes/index.rst +++ b/master/docs/relnotes/index.rst @@ -66,6 +66,11 @@ Features Now, builds started with a :class:`Triggereable` scheduler will be cancelled, while other builds will be retried. The master will make sure that all latent workers are stopped. +* The ``MessageFormatter`` class also allows inline-templates with the ``template`` parameter. + +* The ``MessageFormatter`` class allows custom mail's subjects with the ``subject`` and ``subject_name`` parameters. + +* The ``MessageFormatter`` class allows extending the context given to the Templates via the ``ctx`` parameter. .. _Hyper: https://hyper.sh @@ -127,6 +132,8 @@ Deprecations, Removals, and Non-Compatible Changes * The ``user`` and ``password`` parameters of the ``HttpStatusPush`` reporter have been deprecated in favor of the ``auth`` parameter. +* The ``template_name`` parameter of the ``MessageFormatter`` class has been deprecated in favor of ``template_filename``. + Buildslave ---------- diff --git a/master/setup.py b/master/setup.py index cd7847215f5..a948ccd077c 100755 --- a/master/setup.py +++ b/master/setup.py @@ -299,13 +299,13 @@ def define_plugin_entries(groups): ]), ('buildbot.reporters', [ ('buildbot.reporters.mail', ['MailNotifier']), + ('buildbot.reporters.message', ['MessageFormatter']), ('buildbot.reporters.gerrit', ['GerritStatusPush']), ('buildbot.reporters.http', ['HttpStatusPush']), ('buildbot.reporters.github', ['GitHubStatusPush']), ('buildbot.reporters.stash', ['StashStatusPush']), ('buildbot.reporters.bitbucket', ['BitbucketStatusPush']), ('buildbot.reporters.irc', ['IRC']), - ]), ('buildbot.util', [ # Connection seems to be a way too generic name, though