blob: d51c4b048c10d40dc582beb52938b9bfb0158ef1 [file] [log] [blame]
# Copyright (c) 2013 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.
""" Monkeypatches to override upstream code. """
from buildbot.changes.gitpoller import GitPoller
from buildbot.process.properties import Properties
from buildbot.schedulers.trysched import BadJobfile
from buildbot.status.builder import EXCEPTION, FAILURE
from buildbot.status.web import base as webstatus_base
from buildbot.status.web.status_json import BuilderJsonResource
from buildbot.status.web.status_json import BuildersJsonResource
from buildbot.status.web.status_json import ChangeSourcesJsonResource
from buildbot.status.web.status_json import JsonResource
from buildbot.status.web.status_json import JsonStatusResource
from buildbot.status.web.status_json import MetricsJsonResource
from buildbot.status.web.status_json import ProjectJsonResource
from buildbot.status.web.status_json import SlavesJsonResource
from master import build_utils
from master import chromium_notifier
from master import gatekeeper
from master import try_job_base
from master import try_job_rietveld
from master import try_job_svn
from master.try_job_base import text_to_dict
from twisted.internet import defer
from twisted.python import log
from twisted.web import server
from webstatus import builder_statuses
import builder_name_schema
import config_private
import json
import master_revision
import re
import slave_hosts_cfg
import slaves_cfg
import skia_vars
import utils
# The following users are allowed to run trybots even though they do not have
# accounts in google.com or chromium.org
TRYBOTS_REQUESTER_WHITELIST = [
'henrik.smiding@intel.com',
'kkinnunen@nvidia.com',
'ravimist@gmail.com'
]
################################################################################
############################# Trybot Monkeypatches #############################
################################################################################
@defer.deferredGenerator
def SubmitTryJobChanges(self, changes):
""" Override of SVNPoller.submit_changes:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/try_job_svn.py?view=markup
We modify it so that the patch file url is added to the build properties.
This allows the slave to download the patch directly rather than receiving
it from the master.
"""
for chdict in changes:
# pylint: disable=E1101
parsed = self.parent.parse_options(text_to_dict(chdict['comments']))
# 'fix' revision.
# LKGR must be known before creating the change object.
wfd = defer.waitForDeferred(self.parent.get_lkgr(parsed))
yield wfd
wfd.getResult()
wfd = defer.waitForDeferred(self.master.addChange(
author=','.join(parsed['email']),
revision=parsed['revision'],
comments='',
properties={'patch_file_url': chdict['repository'] + '/' + \
chdict['files'][0]}))
yield wfd
change = wfd.getResult()
self.parent.addChangeInner(chdict['files'], parsed, change.number)
try_job_svn.SVNPoller.submit_changes = SubmitTryJobChanges
def TryJobCreateBuildset(self, ssid, parsed_job):
""" Override of TryJobBase.create_buildset:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/try_job_base.py?view=markup
We modify it to verify that the requested builders are in the builder pool for
this try scheduler. This prevents try requests from running on builders which
are not registered as trybots. This apparently isn't a problem for Chromium
since they use a separate try master.
"""
log.msg('Creating try job(s) %s' % ssid)
result = None
for builder in parsed_job['bot']:
if builder in self.pools[self.name]:
result = self.addBuildsetForSourceStamp(ssid=ssid,
reason=parsed_job['name'],
external_idstring=parsed_job['name'],
builderNames=[builder],
properties=self.get_props(builder, parsed_job))
else:
log.msg('Scheduler: %s rejecting try job for builder: %s not in %s' % (
self.name,
builder,
self.pools[self.name]))
log.msg('Returning buildset: %s' % result)
return result
try_job_base.TryJobBase.create_buildset = TryJobCreateBuildset
def HtmlResourceRender(self, request):
""" Override of buildbot.status.web.base.HtmlResource.render:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/third_party/buildbot_8_4p1/buildbot/status/web/base.py?view=markup
We modify it to pass additional variables on to the web status pages, and
remove the "if False" section.
"""
# tell the WebStatus about the HTTPChannel that got opened, so they
# can close it if we get reconfigured and the WebStatus goes away.
# They keep a weakref to this, since chances are good that it will be
# closed by the browser or by us before we get reconfigured. See
# ticket #102 for details.
if hasattr(request, "channel"):
# web.distrib.Request has no .channel
request.site.buildbot_service.registerChannel(request.channel)
ctx = self.getContext(request)
############################## Added by borenet ##############################
status = self.getStatus(request)
all_builders = status.getBuilderNames()
all_full_category_names = set()
all_categories = set()
all_subcategories = set()
subcategories_by_category = {}
for builder_name in all_builders:
category_full = status.getBuilder(builder_name).category or 'default'
all_full_category_names.add(category_full)
category_split = category_full.split('|')
category = category_split[0]
subcategory = category_split[1] if len(category_split) > 1 else 'default'
all_categories.add(category)
all_subcategories.add(subcategory)
if not subcategories_by_category.get(category):
subcategories_by_category[category] = []
if not subcategory in subcategories_by_category[category]:
subcategories_by_category[category].append(subcategory)
ctx['tree_status_baseurl'] = \
skia_vars.GetGlobalVariable('tree_status_baseurl')
ctx['all_full_category_names'] = sorted(list(all_full_category_names))
ctx['all_categories'] = sorted(list(all_categories))
ctx['all_subcategories'] = sorted(list(all_subcategories))
ctx['subcategories_by_category'] = subcategories_by_category
ctx['default_refresh'] = \
skia_vars.GetGlobalVariable('default_webstatus_refresh')
ctx['skia_repo'] = config_private.SKIA_GIT_URL
active_master = config_private.Master.get_active_master()
ctx['internal_port'] = active_master.master_port
ctx['external_port'] = active_master.master_port_alt
ctx['title_url'] = config_private.Master.Skia.project_url
ctx['slave_hosts_cfg'] = slave_hosts_cfg.SLAVE_HOSTS
ctx['slaves_cfg'] = slaves_cfg.SLAVES
ctx['active_master_name'] = active_master.project_name
ctx['master_revision'] = utils.get_current_revision()
ctx['master_running_revision'] = active_master.running_revision
ctx['master_launch_datetime'] = active_master.launch_datetime
ctx['is_internal_view'] = request.host.port == ctx['internal_port']
ctx['masters'] = []
for master in config_private.Master.valid_masters:
ctx['masters'].append({
'name': master.project_name,
'host': master.master_host,
'internal_port': master.master_port,
'external_port': master.master_port_alt,
})
##############################################################################
d = defer.maybeDeferred(lambda : self.content(request, ctx))
def handle(data):
if isinstance(data, unicode):
data = data.encode("utf-8")
request.setHeader("content-type", self.contentType)
if request.method == "HEAD":
request.setHeader("content-length", len(data))
return ''
return data
d.addCallback(handle)
def ok(data):
request.write(data)
request.finish()
def fail(f):
request.processingFailed(f)
return None # processingFailed will log this for us
d.addCallbacks(ok, fail)
return server.NOT_DONE_YET
webstatus_base.HtmlResource.render = HtmlResourceRender
class TryBuildersJsonResource(JsonResource):
""" Clone of buildbot.status.web.status_json.BuildersJsonResource:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/third_party/buildbot_8_4p1/buildbot/status/web/status_json.py?view=markup
We add filtering to display only the try builders.
"""
help = """List of all the try builders defined on a master."""
pageTitle = 'Builders'
def __init__(self, status, include_only_cq_trybots=False):
JsonResource.__init__(self, status)
for builder_name in self.status.getBuilderNames():
if builder_name_schema.IsTrybot(builder_name) and (
not include_only_cq_trybots or builder_name in slaves_cfg.CQ_TRYBOTS):
self.putChild(builder_name,
BuilderJsonResource(status,
status.getBuilder(builder_name)))
class CQRequiredStepsJsonResource(JsonResource):
help = 'List the steps which cannot fail on the commit queue.'
pageTitle = 'CQ Required Steps'
def asDict(self, request):
return {'cq_required_steps':
skia_vars.GetGlobalVariable('cq_required_steps')}
def JsonStatusResourceInit(self, status):
""" Override of buildbot.status.web.status_json.JsonStatusResource.__init__:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/third_party/buildbot_8_4p1/buildbot/status/web/status_json.py?view=markup
We add trybots, cqtrybots, cq_required_steps (details below).
"""
JsonResource.__init__(self, status)
self.level = 1
self.putChild('builders', BuildersJsonResource(status))
self.putChild('change_sources', ChangeSourcesJsonResource(status))
self.putChild('project', ProjectJsonResource(status))
self.putChild('slaves', SlavesJsonResource(status))
self.putChild('metrics', MetricsJsonResource(status))
############################## Added by borenet ##############################
# Added to address: https://code.google.com/p/skia/issues/detail?id=1134
self.putChild('trybots', TryBuildersJsonResource(status))
##############################################################################
############################## Added by rmistry ##############################
# Added to have a place to get the list of trybots run by the CQ.
self.putChild('cqtrybots',
TryBuildersJsonResource(status, include_only_cq_trybots=True))
##############################################################################
############################## Added by borenet ##############################
# Added to have a place to get the list of steps which cannot fail on the CQ.
self.putChild('cq_required_steps', CQRequiredStepsJsonResource(status))
##############################################################################
############################## Added by borenet ##############################
# Added to have a way to determine which code revision the master is running.
self.putChild('master_revision',
master_revision.MasterCheckedOutRevisionJsonResource(status))
running_rev = config_private.Master.get_active_master().running_revision
self.putChild('master_running_revision',
master_revision.MasterRunningRevisionJsonResource(
status=status, running_revision=running_rev))
# This page gives the result of the most recent build for each builder.
self.putChild('builder_statuses',
builder_statuses.BuilderStatusesJsonResource(status))
##############################################################################
# This needs to be called before the first HelpResource().body call.
self.hackExamples()
JsonStatusResource.__init__ = JsonStatusResourceInit
@defer.deferredGenerator
def TryJobRietveldSubmitJobs(self, jobs):
""" Override of master.try_job_rietveld.TryJobRietveld.SubmitJobs:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/try_job_rietveld.py?view=markup
We modify it to include "baseurl" as a build property.
"""
log.msg('TryJobRietveld.SubmitJobs: %s' % json.dumps(jobs, indent=2))
for job in jobs:
try:
# Gate the try job on the user that requested the job, not the one that
# authored the CL.
# pylint: disable=W0212
########################## Added by rmistry ##########################
if (job.get('requester') and not job['requester'].endswith('@google.com')
and not job['requester'].endswith('@chromium.org')
and not job['requester'] in TRYBOTS_REQUESTER_WHITELIST):
# Reject the job only if the requester has an email not ending in
# google.com or chromium.org
raise BadJobfile(
'TryJobRietveld rejecting job from %s' % job['requester'])
######################################################################
########################## Added by borenet ##########################
if not (job.get('baseurl') and
config_private.Master.Skia.project_name.lower() in
job['baseurl']):
raise BadJobfile('TryJobRietveld rejecting job with unknown baseurl: %s'
% job.get('baseurl'))
######################################################################
if job['email'] != job['requester']:
# Note the fact the try job was requested by someone else in the
# 'reason'.
job['reason'] = job.get('reason') or ''
if job['reason']:
job['reason'] += '; '
job['reason'] += "This CL was triggered by %s" % job['requester']
options = {
'bot': {job['builder']: job['tests']},
'email': [job['email']],
'project': [self._project],
'try_job_key': job['key'],
}
# Transform some properties as is expected by parse_options().
for key in (
########################## Added by borenet ##########################
'baseurl',
######################################################################
'name', 'user', 'root', 'reason', 'clobber', 'patchset',
'issue', 'requester', 'revision'):
options[key] = [job[key]]
# Now cleanup the job dictionary and submit it.
cleaned_job = self.parse_options(options)
wfd = defer.waitForDeferred(self.get_lkgr(cleaned_job))
yield wfd
wfd.getResult()
wfd = defer.waitForDeferred(self.master.addChange(
author=','.join(cleaned_job['email']),
# TODO(maruel): Get patchset properties to get the list of files.
# files=[],
revision=cleaned_job['revision'],
comments=''))
yield wfd
changeids = [wfd.getResult().number]
wfd = defer.waitForDeferred(self.SubmitJob(cleaned_job, changeids))
yield wfd
wfd.getResult()
except BadJobfile, e:
# We need to mark it as failed otherwise it'll stay in the pending
# state. Simulate a buildFinished event on the build.
if not job.get('key'):
log.err(
'Got %s for issue %s but not key, not updating Rietveld' %
(e, job.get('issue')))
continue
log.err(
'Got %s for issue %s, updating Rietveld' % (e, job.get('issue')))
for service in self.master.services:
if service.__class__.__name__ == 'TryServerHttpStatusPush':
# pylint: disable=W0212,W0612
build = {
'properties': [
('buildername', job.get('builder'), None),
('buildnumber', -1, None),
('issue', job['issue'], None),
('patchset', job['patchset'], None),
('project', self._project, None),
('revision', '', None),
('slavename', '', None),
('try_job_key', job['key'], None),
],
'reason': job.get('reason', ''),
# Use EXCEPTION until SKIPPED results in a non-green try job
# results on Rietveld.
'results': EXCEPTION,
}
########################## Added by rmistry #########################
# Do not update Rietveld to mark the try job request as failed.
# See https://code.google.com/p/chromium/issues/detail?id=224014 for
# more context.
# service.push('buildFinished', build=build)
#####################################################################
break
try_job_rietveld.TryJobRietveld.SubmitJobs = TryJobRietveldSubmitJobs
def TryJobBaseGetProps(self, builder, options):
""" Override of try_job_base.TryJobBase.get_props:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/try_job_base.py?view=markup
We modify it to add "baseurl".
"""
keys = (
############################### Added by borenet ###############################
'baseurl',
################################################################################
'clobber',
'issue',
'patchset',
'requester',
'rietveld',
'root',
'try_job_key',
)
# All these settings have no meaning when False or not set, so don't set
# them in that case.
properties = dict((i, options[i]) for i in keys if options.get(i))
properties['testfilter'] = options['bot'].get(builder, None)
# pylint: disable=W0212
props = Properties()
props.updateFromProperties(self.properties)
props.update(properties, self._PROPERTY_SOURCE)
return props
try_job_base.TryJobBase.get_props = TryJobBaseGetProps
def TryJobRietveldConstructor(
self, name, pools, properties=None, last_good_urls=None,
code_review_sites=None, project=None):
try_job_base.TryJobBase.__init__(self, name, pools, properties,
last_good_urls, code_review_sites)
# pylint: disable=W0212
endpoint = self._GetRietveldEndPointForProject(code_review_sites, project)
############################### Added by rmistry ###############################
# rmistry: Adding '&master=tryserver.skia' to the endpoint to help filter the
# number of pending try patchsets returned. More details are in
# https://code.google.com/p/skia/issues/detail?id=2659
endpoint += '&master=tryserver.skia'
# rmistry: Increased the polling time from 10 seconds to 1 min because 10
# seconds is too short for us. The RietveldPoller stops working if the time is
# too short.
# pylint: disable=W0212
self._poller = try_job_rietveld._RietveldPoller(endpoint, interval=60)
################################################################################
# pylint: disable=W0212
self._valid_users = try_job_rietveld._ValidUserPoller(interval=12 * 60 * 60)
self._project = project
log.msg('TryJobRietveld created, get_pending_endpoint=%s '
'project=%s' % (endpoint, project))
try_job_rietveld.TryJobRietveld.__init__ = TryJobRietveldConstructor
class SkiaGateKeeper(gatekeeper.GateKeeper):
def isInterestingBuilder(self, builder_status):
""" Override of gatekeeper.GateKeeper.isInterestingBuilder:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/gatekeeper.py?view=markup
We modify it to actually check whether the builder should be considered by
the GateKeeper, as indicated in its category name.
"""
ret = (not builder_name_schema.IsTrybot(builder_status.getName()) and
chromium_notifier.ChromiumNotifier.isInterestingBuilder(self,
builder_status))
log.msg('[gatekeeper-debug2] ======================')
log.msg('[gatekeeper-debug2] is not trybot: %s' % (
not builder_name_schema.IsTrybot(builder_status.getName())))
log.msg('[gatekeeper-debug2] isInterestingBuilder: %s' % (
chromium_notifier.ChromiumNotifier.isInterestingBuilder(
self, builder_status)))
log.msg('[gatekeeper-debug2] builder_status.getName(): %s' % (
builder_status.getName()))
log.msg('[gatekeeper-debug2] ret: %s' % ret)
log.msg('[gatekeeper-debug2] ======================')
return ret
def isInterestingStep(self, build_status, step_status, results):
""" Override of gatekeeper.GateKeeper.isInterestingStep:
http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/gatekeeper.py?view=markup
We modify it to comment out the SVN revision comparision to determine if the
current build is older because Skia uses commit hashes.
"""
# If we have not failed, or are not interested in this builder,
# then we have nothing to do.
if results[0] != FAILURE:
return False
# Check if the slave is still alive. We should not close the tree for
# inactive slaves.
slave_name = build_status.getSlavename()
if slave_name in self.master_status.getSlaveNames():
# @type self.master_status: L{buildbot.status.builder.Status}
# @type self.parent: L{buildbot.master.BuildMaster}
# @rtype getSlave(): L{buildbot.status.builder.SlaveStatus}
slave_status = self.master_status.getSlave(slave_name)
if slave_status and not slave_status.isConnected():
log.msg('[gatekeeper] Slave %s was disconnected, '
'not closing the tree' % slave_name)
return False
# If the previous build step failed with the same result, we don't care
# about this step.
previous_build_status = build_status.getPreviousBuild()
if previous_build_status:
step_name = self.getName(step_status)
step_type = self.getGenericName(step_name)
previous_steps = [step for step in previous_build_status.getSteps()
if self.getGenericName(self.getName(step)) == step_type]
if len(previous_steps) == 1:
if previous_steps[0].getResults()[0] == FAILURE:
log.msg('[gatekeeper] Slave %s failed, but previously failed on '
'the same step (%s). So not closing tree.' % (
(step_name, slave_name)))
return False
else:
log.msg('[gatekeeper] len(previous_steps) == %d which is weird' %
len(previous_steps))
# If check_revisions=False that means that the tree closure request is
# coming from nightly scheduled bots, that need not necessarily have the
# revision info.
if not self.check_revisions:
return True
# If we don't have a version stamp nor a blame list, then this is most
# likely a build started manually, and we don't want to close the
# tree.
latest_revision = build_utils.getLatestRevision(build_status)
if not latest_revision or not build_status.getResponsibleUsers():
log.msg('[gatekeeper] Slave %s failed, but no version stamp, '
'so skipping.' % slave_name)
return False
# If the tree is open, we don't want to close it again for the same
# revision, or an earlier one in case the build that just finished is a
# slow one and we already fixed the problem and manually opened the tree.
############################### Added by rmistry ###########################
# rmistry: Commenting out the below SVN revision comparision because Skia
# uses commit hashes.
# TODO(rmistry): Figure out how to ensure that previous builds do not close
# the tree again.
#
# if latest_revision <= self._last_closure_revision:
# log.msg('[gatekeeper] Slave %s failed, but we already closed it '
# 'for a previous revision (old=%s, new=%s)' % (
# slave_name, str(self._last_closure_revision),
# str(latest_revision)))
# return False
###########################################################################
log.msg('[gatekeeper] Decided to close tree because of slave %s '
'on revision %s' % (slave_name, str(latest_revision)))
# Up to here, in theory we'd check if the tree is closed but this is too
# slow to check here. Instead, take a look only when we want to close the
# tree.
return True
# Fix try_job_base.TryJobBase._EMAIL_VALIDATOR to handle *.info. This was fixed
# in https://codereview.chromium.org/216293005 but we need this monkeypatch to
# pick it up without a DEPS roll.
try_job_base.TryJobBase._EMAIL_VALIDATOR = re.compile(
r'[a-zA-Z0-9][a-zA-Z0-9\.\+\-\_]*@[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,}$')
# Add logging to GitPoller._stop_on_failure
def _stop_on_failure(self, f):
"utility method to stop the service when a failure occurs"
log.err('GitPoller stopping due to failure: %s' % str(f))
if self.running:
d = defer.maybeDeferred(lambda : self.stopService())
d.addErrback(log.err, 'while stopping broken GitPoller service')
return f
GitPoller._stop_on_failure = _stop_on_failure