blob: 8692f6d435d638913be8526c2e08ba517ded7bcf [file] [log] [blame] [edit]
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
""" Source file for buildbot.status module modifications. """
import urllib
import buildbot
import buildbot.util
import buildbot.status.web.base as base
import buildbot.status.web.console as console
import buildbot.status.web.waterfall as waterfall
import buildbot.status.builder as statusbuilder
import buildbot.status.web.changes
from twisted.python import log
from twisted.python import components
from twisted.web import html
from zope.interface import declarations
# pylint: disable=F0401
import jinja2
import re
import urllib
from master.third_party import stats_bb8 as stats
# A simplified version of the default 'shortrev' macro used in some jinja
# templates. This version isn't actually used for template processing; rather,
# it's used to shorten revision names in the waterfall view, which doesn't make
# much use of jinja templates.
def _ShortRev(rev):
if rev is not None:
return rev[:12]
class BuildBox(waterfall.BuildBox):
"""Build the yellow starting-build box for a waterfall column.
This subclass adds the builder's name to the box. It's otherwise identical
to the parent class, apart from naming syntax.
"""
def getBox(self, req):
b = self.original
number = b.getNumber()
url = base.path_to_build(req, b)
reason = b.getReason()
if reason:
text = (('%s<br><a href="%s">Build %d</a><br>%s')
% (b.getBuilder().getName(), url, number, html.escape(reason)))
else:
text = ('%s<br><a href="%s">Build %d</a>'
% (b.getBuilder().getName(), url, number))
color = "yellow"
class_ = "start"
if b.isFinished() and not b.getSteps():
# the steps have been pruned, so there won't be any indication
# of whether it succeeded or failed. Color the box red or green
# to show its status
color = b.getColor()
class_ = base.build_get_class(b)
return base.Box([text], color=color, class_="BuildStep " + class_)
# First unregister ICurrentBox registered by waterfall.py.
# We won't be able to unregister it without messing with ALLOW_DUPLICATES
# in twisted.python.components. Instead, work directly with adapter to
# remove the component:
origInterface = statusbuilder.BuildStatus
origInterface = declarations.implementedBy(origInterface)
registry = components.getRegistry()
registry.register([origInterface], base.IBox, '', None)
components.registerAdapter(BuildBox, statusbuilder.BuildStatus, base.IBox)
class ChromiumStepBox(waterfall.StepBox):
"""Apply _ShortRev to the revision numbers in 'update' status boxes."""
def getBox(self, req):
text = self.original.getText()
if len(text) > 1 and (text[0] == 'update' or text[0] == 'updating'):
text[1] = _ShortRev(text[1])
return waterfall.StepBox.getBox(self, req)
origInterface = statusbuilder.BuildStepStatus
origInterface = declarations.implementedBy(origInterface)
registry = components.getRegistry()
registry.register([origInterface], base.IBox, '', None)
components.registerAdapter(ChromiumStepBox,
statusbuilder.BuildStepStatus,
base.IBox)
class ChromiumChangeBox(buildbot.status.web.changes.ChangeBox):
"""Apply _ShortRev to the change boxes in the left-hand column."""
def getBox(self, req):
url = req.childLink("../changes/%d" % self.original.number)
template = \
req.site.buildbot_service.templates.get_template("change_macros.html")
who = '@'.join(self.original.getShortAuthor().split('@')[0:2])
revision = _ShortRev(self.original.revision)
text = template.module.box_contents(url=url,
who=who,
pageTitle=self.original.comments,
revision=revision,
project=self.original.project)
return base.Box([text],
class_=' '.join(filter(None, ["Change", self.original.project])))
origInterface = buildbot.changes.changes.Change
origInterface = declarations.implementedBy(origInterface)
registry = components.getRegistry()
registry.register([origInterface], base.IBox, '', None)
components.registerAdapter(ChromiumChangeBox,
buildbot.changes.changes.Change,
base.IBox)
class HorizontalOneBoxPerBuilder(base.HtmlResource):
"""This shows a table with one cell per build. The color of the cell is
the state of the most recently completed build. If there is a build in
progress, the ETA is shown in table cell. The table cell links to the page
for that builder. They are layed out, you guessed it, horizontally.
builder=: show only builds for this builder. Multiple builder= arguments
can be used to see builds from any builder in the set. If no
builder= is given, shows them all.
"""
def __init__(self, builder_filter_fn=None, *args, **kwargs):
super(HorizontalOneBoxPerBuilder, self).__init__(*args, **kwargs)
self.builder_filter_fn = builder_filter_fn or (lambda b: True)
# pylint: disable=W0221
def content(self, request, cxt):
status = self.getStatus(request)
builders = request.args.get("builder", status.getBuilderNames())
cxt_builders = []
categories = set()
for builder_name in builders:
if not self.builder_filter_fn(builder_name):
continue
try:
builder_status = status.getBuilder(builder_name)
except KeyError:
log.msg('status.getBuilder(%r) failed' % builder_name)
continue
classname = base.ITopBox(builder_status).getBox(request).class_
title = builder_name
show_name = 'off' not in request.args.get('titles', ['off'])
url = (base.path_to_root(request) + "waterfall?builder=" +
urllib.quote(builder_name, safe='() '))
category = builder_status.getCategory()
if category:
category = category.split('|')[0]
categories.add(category)
cxt_builders.append({'outcome': classname,
'name': title,
'url': url,
'show_name': show_name,
'category': category})
cxt['builders'] = cxt_builders
cxt['categories'] = sorted(categories)
templates = request.site.buildbot_service.templates
template = templates.get_template("horizontal_one_box_per_build.html")
data = template.render(cxt)
return data
class WaterfallStatusResource(waterfall.WaterfallStatusResource):
"""Don't serve up waterfall pages for builds older than the threshold."""
# pylint: disable=W0221
def content(self, request, cxt):
cutoff = buildbot.util.now() - (60 * 60 * 24 * 2)
last_time = int(request.args.get('last_time', [0])[0])
if (last_time and
last_time < cutoff and
not request.args.get('force', [False])[0]):
return """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Buildbot Waterfall Error</title></head>
<body><p>To prevent DOS of the waterfall, heavy requests like this
are blocked. If you know what you are doing, ask a Chromium trooper
how to bypass the protection.</p></body></html>
"""
# pylint: disable=E1121
return waterfall.WaterfallStatusResource.content(self, request, cxt)
class ConsoleStatusResource(console.ConsoleStatusResource):
"""Class that overrides default behavior of console.ConsoleStatusResource."""
def __init__(self, tagComparator=None, **kwargs):
"""buildbot's TimeRevisionComparator doesn't work exactly right for git.
Allow it to be overridden with the tagComparator parameter."""
super(ConsoleStatusResource, self).__init__(**kwargs)
if tagComparator:
self.comparator = tagComparator
def displayPage(self, *args, **kwargs):
"""Strip the 'who' parameter down to a valid e-mail address."""
result = console.ConsoleStatusResource.displayPage(self, *args, **kwargs)
for revision in result['revisions']:
if 'who' not in revision:
continue
revision['who'] = '@'.join(revision['who'].split('@')[0:2])
return result
def BestRevisionJson(self, request, cxt):
data_fmt = '<html><head></head><body>\n%s</body></html>\n'
revision_fmt = '{\'revision\': \'%s\',\'results\' : [%s]}'
build_fmt = '{\'url\':\'color\': \'%s\', \'name\': \'%s\'}'
status = self.getStatus(request)
debugInfo = {}
d = self.getAllChanges(request, status, debugInfo)
def got_changes(allChanges):
debugInfo["from_cache"] = 0
debugInfo["added_blocks"] = 0
debugInfo["source_all"] = len(allChanges)
revisions = list(self.filterRevisions(allChanges, max_revs=40,
filter={}))
debugInfo["revision_final"] = len(revisions)
# Fetch all the builds for all builders until we get the next build
# after lastRevision.
revision_text = []
if revisions:
lastRevision = revisions[-1].revision
debugInfo["last_revision"] = lastRevision
(builderList, allBuilds) = self.getAllBuildsForRevision(
status, request, lastRevision, 40, [], [], debugInfo, revisions)
for revision in revisions:
builds = self.displayStatusLine(builderList,
allBuilds,
revision,
debugInfo)[0]
build_text = []
for builder_builds in builds.values():
for build in builder_builds:
build_text.append(build_fmt % (
build['color'], urllib.quote(build['builderName'])))
revision_text.append(revision_fmt % (
revision.revision, ''.join(build_text)))
return data_fmt % ''.join(revision_text)
d.addCallback(got_changes)
return d
def content(self, request, cxt): # pylint: disable=W0221
"""Override default reload setting"""
if 'json' in request.args and '213' in request.args['json']:
return self.BestRevisionJson(request, cxt)
if 'reload' not in request.args:
request.args['reload'] = ['9999999999']
if not getattr(self.comparator, 'initialized', True):
return """
<html><head></head><body>
<h2>console unavailable</h2>
<p>while buildbot master is starting up. Please try again in a few seconds.</p>
</body></html>
"""
# pylint: disable=E1121
return console.ConsoleStatusResource.content(self, request, cxt)
def SetupChromiumPages(webstatus, tagComparator=None, customEndpoints=None,
console_repo_filter=None, console_builder_filter=None):
"""Add customizations to default web reporting."""
def _tick_filter(n, stride):
n = ((n / stride) + 1) * stride
return [x for x in range(n+1) if x % (n/stride) == 0]
if not customEndpoints:
customEndpoints = {}
orig_shortrev = webstatus.templates.filters['shortrev']
webstatus.templates.filters.update(
{'shortrev': lambda rev, repo: orig_shortrev(rev, repo).rstrip('.'),
'longrev': lambda x, y: jinja2.escape(unicode(x)),
'numstrip': lambda x: jinja2.escape(unicode(x.lstrip('0123456789'))),
'quote': urllib.quote,
'max': lambda x: reduce(max, x, 0),
'average': lambda x: float(sum(x)) / float(max(len(x), 1)),
'ticks': lambda x: ["{v:%d}" % y for y in _tick_filter(x, 12)],
'addlinks': lambda x: re.sub(
r'(http://[^\s@]+)', r'<a href="\1">\1</a>', x),
'fixname': lambda x: x.translate(None, ' -():'),
'extract_index': lambda x, i: [y[i] for y in x],
})
kwargs = {}
if console_repo_filter:
kwargs['repository'] = console_repo_filter
if console_builder_filter:
kwargs['builder_filter_fn'] = console_builder_filter
console_ = ConsoleStatusResource(
orderByTime=webstatus.orderConsoleByTime,
tagComparator=tagComparator,
**kwargs)
webstatus.putChild("stats", stats.StatsStatusResource())
webstatus.putChild("waterfall", WaterfallStatusResource())
webstatus.putChild("console", console_)
webstatus.putChild("grid", console_)
webstatus.putChild("tgrid", console_)
webstatus.putChild("horizontal_one_box_per_builder",
HorizontalOneBoxPerBuilder(
builder_filter_fn=console_builder_filter))
for url, resource in customEndpoints.items():
webstatus.putChild(url, resource)
return webstatus