blob: 22579bb1a2a30f72206997313f9f2e7caca00cdc [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2011 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.
"""
A library to generate and store the manifests for cros builders to use.
"""
import logging
import os
import re
import time
from xml.dom import minidom
from chromite.buildbot import constants
from chromite.buildbot import manifest_version
from chromite.lib import cros_build_lib as cros_lib
# Paladin constants for manifest names.
PALADIN_COMMIT_ELEMENT = 'pending_commit'
PALADIN_PROJECT_ATTR = 'project'
PALADIN_CHANGE_ID_ATTR = 'change_id'
PALADIN_COMMIT_ATTR = 'commit'
class PromoteCandidateException(Exception):
"""Exception thrown for failure to promote manifest candidate."""
pass
def PrintLink(text, url):
"""Prints out a link to buildbot."""
print '\n@@@STEP_LINK@%(text)s@%(url)s@@@' % { 'text': text, 'url': url }
def _SyncGitRepo(local_dir):
""""Clone Given git repo
Args:
local_dir: location with repo that should be synced.
"""
cros_lib.RunCommand(['git', 'remote', 'update'], cwd=local_dir)
cros_lib.RunCommand(['git', 'rebase', 'origin/master'], cwd=local_dir)
class _LKGMCandidateInfo(manifest_version.VersionInfo):
"""Class to encapsualte the chrome os lkgm candidate info
You can instantiate this class in two ways.
1)using a version file, specifically chromeos_version.sh,
which contains the version information.
2) just passing in the 4 version components (major, minor, sp, patch and
revision number),
Args:
You can instantiate this class in two ways.
1)using a version file, specifically chromeos_version.sh,
which contains the version information.
2) passing in a string with the 3 version components + revision e.g. 41.0.0-r1
Args:
version_string: Optional 3 component version string to parse. Contains:
build_number: release build number.
branch_build_number: current build number on a branch.
patch_number: patch number.
revision_number: version revision
chrome_branch: If version_string specified, specify chrome_branch i.e. 13.
version_file: version file location.
"""
LKGM_RE = '(\d+\.\d+\.\d+)(?:-rc(\d+))?'
def __init__(self, version_string=None, chrome_branch=None,
version_file=None):
self.revision_number = None
if version_string:
match = re.search(self.LKGM_RE, version_string)
assert match, 'LKGM did not re %s' % self.LKGM_RE
super(_LKGMCandidateInfo, self).__init__(match.group(1), chrome_branch,
incr_type='branch')
if match.group(2):
self.revision_number = int(match.group(2))
else:
super(_LKGMCandidateInfo, self).__init__(version_file=version_file,
incr_type='branch')
if not self.revision_number:
self.revision_number = 1
def VersionString(self):
"""returns the full version string of the lkgm candidate"""
return '%s.%s.%s-rc%s' % (self.build_number, self.branch_build_number,
self.patch_number, self.revision_number)
@classmethod
def VersionCompare(cls, version_string):
"""Useful method to return a comparable version of a LKGM string."""
lkgm = cls(version_string)
return map(int, [lkgm.build_number, lkgm.branch_build_number,
lkgm.patch_number, lkgm.revision_number])
def IncrementVersion(self, message=None, dry_run=False):
"""Increments the version by incrementing the revision #."""
self.revision_number += 1
return self.VersionString()
class LKGMManager(manifest_version.BuildSpecsManager):
"""A Class to manage lkgm candidates and their states.
Vars:
lkgm_subdir: Subdirectory within manifest repo to store candidates.
"""
# Max timeout before assuming other builders have failed for Chrome PFQ.
# Longer as there is little to lose for Chrome PFQ waiting and arm
# has been slower often.
CHROME_LONG_MAX_TIMEOUT_SECONDS = 3600
# Max timeout before assuming other builders have failed.
MAX_TIMEOUT_SECONDS = 300
# Polling timeout for checking git repo for other build statuses.
SLEEP_TIMEOUT = 30
# Sub-directories for LKGM and Chrome LKGM's.
LKGM_SUBDIR = 'LKGM-candidates'
CHROME_PFQ_SUBDIR = 'chrome-LKGM-candidates'
COMMIT_QUEUE_SUBDIR = 'paladin'
# Set path in repository to keep latest approved LKGM manifest.
LKGM_PATH = 'LKGM/lkgm.xml'
@classmethod
def GetAbsolutePathToLKGM(cls):
"""Returns the path to the LKGM file blessed by builders."""
return os.path.join(cls._TMP_MANIFEST_DIR, cls.LKGM_PATH)
@classmethod
def GetLKGMVersion(cls):
"""Returns the full buildspec version the LKGM corresponds to."""
realpath = os.path.realpath(cls.GetAbsolutePathToLKGM())
version, _ = os.path.splitext(os.path.basename(realpath))
return version
def __init__(self, source_dir, checkout_repo, manifest_repo, branch,
build_name, build_type, dry_run=True):
"""Initialize an LKGM Manager.
Args:
build_type: Type of build. Must be a pfq type.
Other args see manifest_version.BuildSpecsManager.
"""
super(LKGMManager, self).__init__(
source_dir=source_dir, checkout_repo=checkout_repo,
manifest_repo=manifest_repo, branch=branch, build_name=build_name,
incr_type='branch', dry_run=dry_run)
self.compare_versions_fn = _LKGMCandidateInfo.VersionCompare
self.build_type = build_type
if self.build_type == constants.CHROME_PFQ_TYPE:
self.lkgm_subdir = self.CHROME_PFQ_SUBDIR
elif self.build_type == constants.COMMIT_QUEUE_TYPE:
self.lkgm_subdir = self.COMMIT_QUEUE_SUBDIR
else:
assert self.build_type, constants.PFQ_TYPE
self.lkgm_subdir = self.LKGM_SUBDIR
def _RunLambdaWithTimeout(self, function_to_run, use_long_timeout=False):
"""Runs function_to_run until it returns a value or timeout is reached."""
function_success = False
start_time = time.time()
max_timeout = self.MAX_TIMEOUT_SECONDS
if use_long_timeout:
if self.build_type == constants.PFQ_TYPE:
max_timeout = self.LONG_MAX_TIMEOUT_SECONDS
else:
max_timeout = self.CHROME_LONG_MAX_TIMEOUT_SECONDS
# Monitor the repo until all builders report in or we've waited too long.
while (time.time() - start_time) < max_timeout:
function_success = function_to_run()
if function_success:
break
else:
time.sleep(self.SLEEP_TIMEOUT)
return function_success
def _LoadSpecs(self, version_info):
"""Loads the specifications from the working directory.
Args:
version_info: Info class for version information of cros.
"""
super(LKGMManager, self)._LoadSpecs(version_info, self.lkgm_subdir)
def _GetLatestCandidateByVersion(self, version_info):
"""Returns the latest lkgm candidate corresponding to the version file.
Args:
version_info: Info class for version information of cros.
"""
if self.all:
matched_lkgms = filter(
lambda ver: ver.startswith(version_info.VersionString()), self.all)
if matched_lkgms:
return _LKGMCandidateInfo(sorted(matched_lkgms,
key=self.compare_versions_fn)[-1])
return _LKGMCandidateInfo(version_info.VersionString())
def AddPatchesToManifest(self, manifest, patches):
manifest_dom = minidom.parse(manifest)
for patch in patches:
pending_commit = manifest_dom.createElement(PALADIN_COMMIT_ELEMENT)
pending_commit.setAttribute(PALADIN_PROJECT_ATTR, patch.project)
pending_commit.setAttribute(PALADIN_CHANGE_ID_ATTR, patch.id)
pending_commit.setAttribute(PALADIN_COMMIT_ATTR, patch.commit)
manifest_dom.documentElement.appendChild(pending_commit)
with open(manifest, 'w+') as manifest_file:
manifest_dom.writexml(manifest_file)
def CreateNewCandidate(self, force_version=None, patches=None, retries=3):
"""Gets the version number of the next build spec to build.
Args:
force_version: Forces us to use this version.
patches: An array of GerritPatches that should be built with this
manifest as part of a Commit Queue run.
retries: Number of retries for updating the status.
Returns:
next_build: a string of the next build number for the builder to consume
or None in case of no need to build.
Raises:
GenerateBuildSpecException in case of failure to generate a buildspec
"""
last_error = None
for _ in range(0, retries + 1):
try:
version_info = self._GetCurrentVersionInfo()
self._LoadSpecs(version_info)
lkgm_info = self._GetLatestCandidateByVersion(version_info)
if force_version:
return self.GetLocalManifest(force_version)
self._PrepSpecChanges()
self.current_version = self._CreateNewBuildSpec(lkgm_info)
path_to_new_build_spec = self.GetLocalManifest(self.current_version)
if patches: self.AddPatchesToManifest(path_to_new_build_spec, patches)
if self.current_version:
logging.debug('Using build spec: %s', self.current_version)
commit_message = 'Automatic: Start %s %s' % (self.build_name,
self.current_version)
self._SetInFlight()
self._PushSpecChanges(commit_message)
return path_to_new_build_spec
except (cros_lib.RunCommandError,
manifest_version.GitCommandException) as e:
err_msg = 'Failed to generate LKGM Candidate. error: %s' % e
logging.error(err_msg)
last_error = err_msg
else:
raise manifest_version.GenerateBuildSpecException(last_error)
def GetLatestCandidate(self, force_version=None, retries=5):
"""Gets the version number of the next build spec to build.
Args:
force_version: Forces us to use this version.
retries: Number of retries for updating the status
Returns:
Local path to manifest to build or None in case of no need to build.
Raises:
GenerateBuildSpecException in case of failure to generate a buildspec
"""
def _AttemptToGetLatestCandidate():
"""Attempts to acquire latest candidate using manifest repo."""
version_info = self._GetCurrentVersionInfo()
self._LoadSpecs(version_info)
version_to_use = None
if force_version:
version_to_use = force_version
elif self.latest_unprocessed:
version_to_use = self.latest_unprocessed
else:
logging.info('Found nothing new to build, trying again later.')
return version_to_use
self.current_version = self._RunLambdaWithTimeout(
_AttemptToGetLatestCandidate)
if self.current_version:
last_error = None
for _ in range(0, retries + 1):
try:
logging.debug('Using build spec: %s', self.current_version)
commit_message = 'Automatic: Start %s %s' % (self.build_name,
self.current_version)
self._PrepSpecChanges()
self._SetInFlight()
self._PushSpecChanges(commit_message)
break
except (cros_lib.RunCommandError,
manifest_version.GitCommandException) as e:
err_msg = 'Failed to set LKGM Candidate inflight. error: %s' % e
logging.error(err_msg)
last_error = err_msg
else:
raise manifest_version.GenerateBuildSpecException(last_error)
return self.GetLocalManifest(self.current_version)
def GetBuildersStatus(self, builders_array, version_file):
"""Returns a build-names->status dictionary of build statuses."""
builder_statuses = {}
def _CheckStatusOfBuildersArray():
"""Helper function that iterates through current statuses."""
num_complete = 0
_SyncGitRepo(self._TMP_MANIFEST_DIR)
version_info = _LKGMCandidateInfo(version_file=version_file)
for builder in builders_array:
if builder_statuses.get(builder) not in LKGMManager.STATUS_COMPLETED:
logging.debug("Checking for builder %s's status" % builder)
builder_statuses[builder] = self.GetBuildStatus(
builder, self.current_version, version_info)
if builder_statuses[builder] == LKGMManager.STATUS_PASSED:
num_complete += 1
logging.info('Builder %s completed with status passed', builder)
elif builder_statuses[builder] == LKGMManager.STATUS_FAILED:
num_complete += 1
logging.info('Builder %s completed with status failed', builder)
elif not builder_statuses[builder]:
logging.debug('No status found for builder %s.' % builder)
else:
num_complete += 1
if num_complete < len(builders_array):
logging.info('Waiting for other builds to complete')
return None
else:
return 'Builds completed.'
# Check for build completion until all builders report in.
builds_succeeded = self._RunLambdaWithTimeout(_CheckStatusOfBuildersArray,
use_long_timeout=True)
if not builds_succeeded:
logging.error('Not all builds finished before MAX_TIMEOUT reached.')
return builder_statuses
def PromoteCandidate(self, retries=5):
"""Promotes the current LKGM candidate to be a real versioned LKGM."""
assert self.current_version, 'No current manifest exists.'
last_error = None
path_to_candidate = self.GetLocalManifest(self.current_version)
path_to_lkgm = self.GetAbsolutePathToLKGM()
assert os.path.exists(path_to_candidate), 'Candidate not found locally.'
# This may potentially fail for not being at TOT while pushing.
for index in range(0, retries + 1):
try:
self._PrepSpecChanges()
manifest_version.CreateSymlink(path_to_candidate, path_to_lkgm)
cros_lib.RunCommand(['git', 'add', self.LKGM_PATH],
cwd=self._TMP_MANIFEST_DIR)
self._PushSpecChanges(
'Automatic: %s promoting %s to LKGM' % (self.build_name,
self.current_version))
return
except (manifest_version.GitCommandException,
cros_lib.RunCommandError) as e:
last_error = 'Failed to promote manifest. error: %s' % e
logging.error(last_error)
logging.error('Retrying to promote manifest: Retry %d/%d' %
(index + 1, retries))
else:
raise PromoteCandidateException(last_error)
def GenerateBlameListSinceLKGM(self):
"""Prints out links to all CL's that have been committed since LKGM.
Add buildbot trappings to print <a href='url'>text</a> in the waterfall for
each CL committed since we last had a passing build.
"""
handler = cros_lib.ManifestHandler.ParseManifest(
self.GetAbsolutePathToLKGM())
reviewed_on_re = re.compile('\s*Reviewed-on:\s*(\S+)')
author_re = re.compile('\s*Author:.*<(\S+)@\S+>\s*')
for project in handler.projects.keys():
rel_src_path = handler.projects[project].get('path')
# If it's not part of our source tree, it doesn't affect our build.
if not rel_src_path:
continue
src_path = self.cros_source.GetRelativePath(rel_src_path)
# Additional case in case the repo has been removed from the manifest.
if not os.path.exists(src_path):
cros_lib.Info('Detected repo removed from manifest %s' % project)
continue
revision = handler.projects[project]['revision']
result = cros_lib.RunCommand(['git', 'log', '%s..HEAD' % revision],
print_cmd=False, redirect_stdout=True,
cwd=src_path)
current_author = None
for line in result.output.splitlines():
author_match = author_re.match(line)
if author_match:
current_author = author_match.group(1)
review_match = reviewed_on_re.match(line)
if review_match:
review = review_match.group(1)
_, _, change_number = review.rpartition('/')
PrintLink('%s:%s' % (current_author, change_number), review)