| # 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 |