1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16 17import jinja2 18 19from twisted.internet import defer 20 21from buildbot import util 22from buildbot.process.properties import Properties 23from buildbot.process.results import CANCELLED 24from buildbot.process.results import EXCEPTION 25from buildbot.process.results import FAILURE 26from buildbot.process.results import SUCCESS 27from buildbot.process.results import WARNINGS 28from buildbot.process.results import statusToString 29from buildbot.reporters import utils 30from buildbot.warnings import warn_deprecated 31 32 33def get_detected_status_text(mode, results, previous_results): 34 if results == FAILURE: 35 if ('change' in mode or 'problem' in mode) and previous_results is not None \ 36 and previous_results != FAILURE: 37 text = "new failure" 38 else: 39 text = "failed build" 40 elif results == WARNINGS: 41 text = "problem in the build" 42 elif results == SUCCESS: 43 if "change" in mode and previous_results is not None and previous_results != results: 44 text = "restored build" 45 else: 46 text = "passing build" 47 elif results == EXCEPTION: 48 text = "build exception" 49 else: 50 text = "{} build".format(statusToString(results)) 51 52 return text 53 54 55def get_message_summary_text(build, results): 56 t = build['state_string'] 57 if t: 58 t = ": " + t 59 else: 60 t = "" 61 62 if results == SUCCESS: 63 text = "Build succeeded!" 64 elif results == WARNINGS: 65 text = "Build Had Warnings{}".format(t) 66 elif results == CANCELLED: 67 text = "Build was cancelled" 68 else: 69 text = "BUILD FAILED{}".format(t) 70 71 return text 72 73 74def get_message_source_stamp_text(source_stamps): 75 text = "" 76 77 for ss in source_stamps: 78 source = "" 79 80 if ss['branch']: 81 source += "[branch {}] ".format(ss['branch']) 82 83 if ss['revision']: 84 source += str(ss['revision']) 85 else: 86 source += "HEAD" 87 88 if ss['patch'] is not None: 89 source += " (plus patch)" 90 91 discriminator = "" 92 if ss['codebase']: 93 discriminator = " '{}'".format(ss['codebase']) 94 95 text += "Build Source Stamp{}: {}\n".format(discriminator, source) 96 97 return text 98 99 100def get_projects_text(source_stamps, master): 101 projects = set() 102 103 for ss in source_stamps: 104 if ss['project']: 105 projects.add(ss['project']) 106 107 if not projects: 108 projects = [master.config.title] 109 110 return ', '.join(list(projects)) 111 112 113def create_context_for_build(mode, build, master, blamelist): 114 buildset = build['buildset'] 115 ss_list = buildset['sourcestamps'] 116 results = build['results'] 117 118 if 'prev_build' in build and build['prev_build'] is not None: 119 previous_results = build['prev_build']['results'] 120 else: 121 previous_results = None 122 123 return { 124 'results': build['results'], 125 'mode': mode, 126 'buildername': build['builder']['name'], 127 'workername': build['properties'].get('workername', ["<unknown>"])[0], 128 'buildset': buildset, 129 'build': build, 130 'projects': get_projects_text(ss_list, master), 131 'previous_results': previous_results, 132 'status_detected': get_detected_status_text(mode, results, previous_results), 133 'build_url': utils.getURLForBuild(master, build['builder']['builderid'], build['number']), 134 'buildbot_url': master.config.buildbotURL, 135 'blamelist': blamelist, 136 'summary': get_message_summary_text(build, results), 137 'sourcestamps': get_message_source_stamp_text(ss_list) 138 } 139 140 141def create_context_for_worker(master, worker): 142 return { 143 'buildbot_title': master.config.title, 144 'buildbot_url': master.config.buildbotURL, 145 'worker': worker, 146 } 147 148 149class MessageFormatterBase(util.ComparableMixin): 150 151 template_type = 'plain' 152 153 def __init__(self, ctx=None, want_properties=True, wantProperties=None, 154 want_steps=False, wantSteps=None, wantLogs=None, 155 want_logs=False, want_logs_content=False): 156 if ctx is None: 157 ctx = {} 158 self.context = ctx 159 if wantProperties is not None: 160 warn_deprecated('3.4.0', f'{self.__class__.__name__}: wantProperties has been ' 161 'deprecated, use want_properties') 162 self.want_properties = wantProperties 163 else: 164 self.want_properties = want_properties 165 if wantSteps is not None: 166 warn_deprecated('3.4.0', f'{self.__class__.__name__}: wantSteps has been deprecated, ' + 167 'use want_steps') 168 self.want_steps = wantSteps 169 else: 170 self.want_steps = want_steps 171 if wantLogs is not None: 172 warn_deprecated('3.4.0', f'{self.__class__.__name__}: wantLogs has been deprecated, ' + 173 'use want_logs and want_logs_content') 174 else: 175 wantLogs = False 176 177 self.want_logs = want_logs or wantLogs 178 self.want_logs_content = want_logs_content or wantLogs 179 180 def buildAdditionalContext(self, master, ctx): 181 pass 182 183 @defer.inlineCallbacks 184 def render_message_dict(self, master, context): 185 """Generate a buildbot reporter message and return a dictionary 186 containing the message body, type and subject.""" 187 188 ''' This is an informal description of what message dictionaries are expected to be 189 produced. It is an internal API and expected to change even within bugfix releases, if 190 needed. 191 192 The message dictionary contains the 'body', 'type' and 'subject' keys: 193 194 - 'subject' is a string that defines a subject of the message. It's not necessarily 195 used on all reporters. It may be None. 196 197 - 'type' must be 'plain', 'html' or 'json'. 198 199 - 'body' is the content of the message. It may be None. The type of the data depends 200 on the value of the 'type' parameter: 201 202 - 'plain': Must be a string 203 204 - 'html': Must be a string 205 206 - 'json': Must be a non-encoded jsonnable value. The root element must be either 207 of dictionary, list or string. This must not change during all invocations of 208 a particular instance of the formatter. 209 210 In case of a report being created for multiple builds (e.g. in the case of a buildset), 211 the values returned by message formatter are concatenated. If this is not possible 212 (e.g. if the body is a dictionary), any subsequent messages are ignored. 213 ''' 214 yield self.buildAdditionalContext(master, context) 215 context.update(self.context) 216 217 return { 218 'body': (yield self.render_message_body(context)), 219 'type': self.template_type, 220 'subject': (yield self.render_message_subject(context)) 221 } 222 223 def render_message_body(self, context): 224 return None 225 226 def render_message_subject(self, context): 227 return None 228 229 def format_message_for_build(self, master, build, **kwargs): 230 # Known kwargs keys: mode, users 231 raise NotImplementedError 232 233 234class MessageFormatterEmpty(MessageFormatterBase): 235 def format_message_for_build(self, master, build, **kwargs): 236 return { 237 'body': None, 238 'type': 'plain', 239 'subject': None 240 } 241 242 243class MessageFormatterFunction(MessageFormatterBase): 244 245 def __init__(self, function, template_type, **kwargs): 246 super().__init__(**kwargs) 247 self.template_type = template_type 248 self._function = function 249 250 @defer.inlineCallbacks 251 def format_message_for_build(self, master, build, **kwargs): 252 msgdict = yield self.render_message_dict(master, {'build': build}) 253 return msgdict 254 255 def render_message_body(self, context): 256 return self._function(context) 257 258 def render_message_subject(self, context): 259 return None 260 261 262class MessageFormatterRenderable(MessageFormatterBase): 263 264 template_type = 'plain' 265 266 def __init__(self, template, subject=None): 267 super().__init__() 268 self.template = template 269 self.subject = subject 270 271 @defer.inlineCallbacks 272 def format_message_for_build(self, master, build, **kwargs): 273 msgdict = yield self.render_message_dict(master, {'build': build, 'master': master}) 274 return msgdict 275 276 @defer.inlineCallbacks 277 def render_message_body(self, context): 278 props = Properties.fromDict(context['build']['properties']) 279 props.master = context['master'] 280 281 body = yield props.render(self.template) 282 return body 283 284 @defer.inlineCallbacks 285 def render_message_subject(self, context): 286 props = Properties.fromDict(context['build']['properties']) 287 props.master = context['master'] 288 289 body = yield props.render(self.subject) 290 return body 291 292 293default_body_template = '''\ 294The Buildbot has detected a {{ status_detected }} on builder {{ buildername }} while building {{ projects }}. 295Full details are available at: 296 {{ build_url }} 297 298Buildbot URL: {{ buildbot_url }} 299 300Worker for this Build: {{ workername }} 301 302Build Reason: {{ build['properties'].get('reason', ["<unknown>"])[0] }} 303Blamelist: {{ ", ".join(blamelist) }} 304 305{{ summary }} 306 307Sincerely, 308 -The Buildbot 309''' # noqa pylint: disable=line-too-long 310 311 312class MessageFormatterBaseJinja(MessageFormatterBase): 313 compare_attrs = ['body_template', 'subject_template', 'template_type'] 314 subject_template = None 315 template_type = 'plain' 316 317 def __init__(self, template=None, subject=None, template_type=None, **kwargs): 318 if template is None: 319 template = default_body_template 320 321 self.body_template = jinja2.Template(template) 322 323 if subject is not None: 324 self.subject_template = jinja2.Template(subject) 325 326 if template_type is not None: 327 self.template_type = template_type 328 329 super().__init__(**kwargs) 330 331 def buildAdditionalContext(self, master, ctx): 332 pass 333 334 def render_message_body(self, context): 335 return self.body_template.render(context) 336 337 def render_message_subject(self, context): 338 if self.subject_template is None: 339 return None 340 return self.subject_template.render(context) 341 342 343class MessageFormatter(MessageFormatterBaseJinja): 344 @defer.inlineCallbacks 345 def format_message_for_build(self, master, build, users=None, mode=None): 346 ctx = create_context_for_build(mode, build, master, users) 347 msgdict = yield self.render_message_dict(master, ctx) 348 return msgdict 349 350 351default_missing_template = '''\ 352The Buildbot working for '{{buildbot_title}}' has noticed that the worker named {{worker.name}} went away. 353 354It last disconnected at {{worker.last_connection}}. 355 356{% if 'admin' in worker['workerinfo'] %} 357The admin on record (as reported by WORKER:info/admin) was {{worker.workerinfo.admin}}. 358{% endif %} 359 360Sincerely, 361 -The Buildbot 362''' # noqa pylint: disable=line-too-long 363 364 365class MessageFormatterMissingWorker(MessageFormatterBaseJinja): 366 template_filename = 'missing_mail.txt' 367 368 def __init__(self, template=None, **kwargs): 369 if template is None: 370 template = default_missing_template 371 super().__init__(template=template, **kwargs) 372 373 @defer.inlineCallbacks 374 def formatMessageForMissingWorker(self, master, worker): 375 ctx = create_context_for_worker(master, worker) 376 msgdict = yield self.render_message_dict(master, ctx) 377 return msgdict 378