blob: c6be41393f05123a8504c1763f83e5053163724a [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Examine success/fail history for Chrome/ium OS builds.
Used to check in a LKGM version for Chrome OS for other consumers.
"""
from __future__ import print_function
import distutils.version
import os
from chromite.cbuildbot import archive_lib
from chromite.cbuildbot import tree_status
from chromite.lib import builder_status_lib
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import gclient
from chromite.lib import gs
from chromite.lib import osutils
from chromite.lib import parallel
class LKGMNotFound(Exception):
"""Raised if a newer valid LKGM could not be found."""
class LKGMNotCommitted(Exception):
"""Raised if we could not submit a new LKGM."""
class ChromeCommitter(object):
"""Committer object responsible for obtaining a new LKGM and committing it."""
_COMMIT_MSG = ('Automated Commit: Committing new LKGM version %(version)s '
'for chromeos.')
_CANDIDATES_TO_CONSIDER = 10
_SLEEP_TIMEOUT = 30
_TREE_TIMEOUT = 7200
def __init__(self, checkout_dir, dryrun):
self._checkout_dir = checkout_dir
self._dryrun = dryrun
self._lkgm = None
self._old_lkgm = None
self.site_config = config_lib.GetConfig()
def CheckoutChromeLKGM(self):
"""Checkout chromeos LKGM file for chrome into tmp checkout dir."""
if not os.path.exists(self._checkout_dir):
cros_build_lib.RunCommand(
['git', 'clone', constants.CHROMIUM_GOB_URL,
self._checkout_dir])
else:
cros_build_lib.RunCommand(
['git', 'fetch', 'origin'], cwd=self._checkout_dir)
cros_build_lib.RunCommand(
['git', 'checkout', '-f', 'origin/master'], cwd=self._checkout_dir)
cros_build_lib.RunCommand(
['git', 'branch', '-D', 'lkgm-roll'], cwd=self._checkout_dir,
error_code_ok=True)
cros_build_lib.RunCommand(
['git', 'checkout', '-b', 'lkgm-roll', 'origin/master'],
cwd=self._checkout_dir)
self._old_lkgm = osutils.ReadFile(
os.path.join(self._checkout_dir, constants.PATH_TO_CHROME_LKGM))
@cros_build_lib.MemoizedSingleCall
def _GetLatestCanaryVersions(self):
"""Returns the latest CANDIDATES_TO_CONSIDER canary versions."""
gs_handle = gs.GSContext()
version_paths = gs_handle.LS(builder_status_lib.BUILD_STATUS_URL)
# Strip gs://<path> prefix and trailing /'s.
versions = [os.path.basename(v.rstrip('/')) for v in version_paths]
lv = distutils.version.LooseVersion
# We only care about canary versions which always end in 0.0.
canary_versions = [v for v in versions if v.endswith('.0.0')]
new_canary_versions = [v for v in canary_versions
if lv(v) > lv(self._old_lkgm)]
return sorted(new_canary_versions, key=lv,
reverse=True)[0:self._CANDIDATES_TO_CONSIDER]
def GetCanariesForChromeLKGM(self):
"""Grabs a list of builders that are important for the Chrome LKGM."""
builders = []
for build_name, conf in self.site_config.iteritems():
if (conf['build_type'] == constants.CANARY_TYPE and
conf['critical_for_chrome'] and not conf['child_configs']):
builders.append(build_name)
return builders
def FindNewLKGM(self):
"""Finds a new LKGM for chrome from previous chromeos releases."""
versions = self._GetLatestCanaryVersions()
if not versions:
raise LKGMNotFound('No valid LKGM found newer than the old LKGM.')
canaries = self.GetCanariesForChromeLKGM()
logging.info('Considering the following versions: %s', ' '.join(versions))
logging.info('Using scores from the following canaries: %s',
' '.join(canaries))
# Scores are based on passing builders.
version_scores = {}
for version in versions:
version_scores[version] = 0
failed_builders = []
for builder in canaries:
status = builder_status_lib.BuilderStatusManager.GetBuilderStatus(
builder, version, retries=0)
if status:
if status.Passed():
if version_scores[version] != -1:
version_scores[version] += 1
elif status.Failed():
# We don't consider builds with any reporting failures.
failed_builders.append(builder)
version_scores[version] = -1
if len(failed_builders) > 0:
logging.warning('Version: %s Failed builders: %s',
version, ', '.join(failed_builders))
logging.info('Version: %s Score: %d', version, version_scores[version])
# We want to get the version with the highest score. In case of a tie, we
# want to choose the highest version.
lkgm = max((v, k) for k, v in version_scores.iteritems())[1]
if not version_scores[lkgm] > 0:
raise LKGMNotFound('No valid LKGM found. Scores are too low.')
self._lkgm = lkgm
def CommitNewLKGM(self):
"""Commits the new LKGM file using our template commit message."""
lv = distutils.version.LooseVersion
if not self._lkgm and not lv(self._lkgm) < lv(self._old_lkgm):
raise LKGMNotFound('No valid LKGM found. Did you run FindNewLKGM?')
commit_msg = self._COMMIT_MSG % dict(version=self._lkgm)
try:
# Add the new versioned file.
osutils.WriteFile(
os.path.join(self._checkout_dir, constants.PATH_TO_CHROME_LKGM),
self._lkgm)
cros_build_lib.RunCommand(
['git', 'add', constants.PATH_TO_CHROME_LKGM], cwd=self._checkout_dir)
# Commit it!
cros_build_lib.RunCommand(
['git', 'commit', '-m', commit_msg],
cwd=self._checkout_dir)
except cros_build_lib.RunCommandError as e:
raise LKGMNotCommitted(
'Could not create git commit with new LKGM: %r' % e)
if not tree_status.IsTreeOpen(status_url=gclient.STATUS_URL,
period=self._SLEEP_TIMEOUT,
timeout=self._TREE_TIMEOUT):
raise LKGMNotCommitted('Chromium Tree is closed')
if not self._dryrun:
try:
cros_build_lib.RunCommand(
['git', 'cl', 'land', '-f', '--bypass-hooks', '-m', commit_msg],
cwd=self._checkout_dir)
except cros_build_lib.RunCommandError as e:
raise LKGMNotCommitted('Could not submit LKGM: %r' % e)
def UpdateLatestFilesForBot(self, config, versions):
"""Update the LATEST files, for a given bot, in Google Storage.
Args:
config: The builder config to update.
versions: Versions of ChromeOS to look at, sorted in descending order.
"""
base_url = archive_lib.GetBaseUploadURI(config)
acl = archive_lib.GetUploadACL(config)
latest_url = None
# gs.GSContext skips over all commands (including read-only checks)
# when dry_run is True, so we have to create two context objects.
# TODO(davidjames): Fix this.
gs_ctx = gs.GSContext()
copy_ctx = gs.GSContext(dry_run=self._dryrun)
for version in reversed(versions):
url = os.path.join(base_url, 'LATEST-%s' % version)
found = gs_ctx.Exists(url, print_cmd=False)
if not found and latest_url:
try:
copy_ctx.Copy(latest_url, url, version=0, acl=acl)
logging.info('Copied %s -> %s', latest_url, url)
except gs.GSContextPreconditionFailed:
found = True
if found:
logging.info('Found %s', url)
latest_url = url
def UpdateLatestFiles(self):
"""Update the LATEST files since LKGM, in Google Storage."""
ext_cfgs, int_cfgs = self.site_config.FindFullConfigsForBoard(board=None)
versions = self._GetLatestCanaryVersions() + [self._old_lkgm]
tasks = [[cfg, versions] for cfg in ext_cfgs + int_cfgs]
parallel.RunTasksInProcessPool(self.UpdateLatestFilesForBot, tasks,
processes=100)
def _GetParser():
"""Returns the parser to use for this module."""
parser = commandline.ArgumentParser(usage=__doc__, caching=True)
parser.add_argument('--dryrun', action='store_true', default=False,
help="Find the next LKGM but don't commit it.")
parser.add_argument('--workdir', default=os.path.join(os.getcwd(), 'src'),
help=('Path to a checkout of chromium/src. '
'Defaults to PWD/src'))
return parser
def main(argv):
parser = _GetParser()
args = parser.parse_args(argv)
committer = ChromeCommitter(args.workdir, dryrun=args.dryrun)
committer.CheckoutChromeLKGM()
committer.UpdateLatestFiles()
committer.FindNewLKGM()
committer.CommitNewLKGM()
return 0