blob: ae65ce7e4a27f7150df18b65ef36f55f44a35e02 [file] [log] [blame]
# Copyright 2016 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.
"""Module containing the config stages."""
from __future__ import print_function
import os
import re
import traceback
from chromite.cbuildbot.stages import generic_stages
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.cbuildbot import repository
from chromite.lib import cros_logging as logging
from chromite.lib import cros_build_lib
from chromite.lib import git
from chromite.lib import gs
from chromite.lib import osutils
GS_GE_TEMPLATE_BUCKET = 'gs://chromeos-build-release-console/'
GS_GE_TEMPLATE_TOT = GS_GE_TEMPLATE_BUCKET + 'build_config.ToT.json'
GS_GE_TEMPLATE_RELEASE = GS_GE_TEMPLATE_BUCKET + 'build_config.release-R*'
GE_BUILD_CONFIG_FILE = 'ge_build_config.json'
class UpdateConfigException(Exception):
"""Failed to update configs."""
class BranchNotFoundException(Exception):
"""Didn't find the corresponding branch."""
def GetWorkDir(project):
"""Return temporary work directory."""
return os.path.join('tmp', 'tmp_%s' % project)
def CreateTmpGitRepo(project, project_url):
"""Create a temporary git repo locally.
Args:
project: git project name to clone.
project_url: git project url to clone.
Returns:
project_dir: local project directory.
"""
work_dir = GetWorkDir(project)
if not cros_build_lib.IsInsideChroot():
work_dir = os.path.join(constants.SOURCE_ROOT,
constants.DEFAULT_CHROOT_DIR,
work_dir)
# Delete the work_dir built by previous runs.
osutils.RmDir(work_dir, ignore_missing=True, sudo=True)
# Safely create work_dir
osutils.SafeMakedirs(work_dir)
project_dir = os.path.join(work_dir, project)
ref = os.path.join(constants.SOURCE_ROOT, project)
logging.info('Cloning %s %s to %s', project_url, ref, project_dir)
repository.CloneWorkingRepo(dest=project_dir,
url=project_url,
reference=ref)
return project_dir
def GetBranchName(template_file):
"""Parse the template gs path and return the right branch name"""
match = re.search(r'build_config\.(.+?)\.json', template_file)
if match:
if match.group(1) == 'ToT':
# Given 'build_config.ToT.json',
# return branch name 'master'.
return 'master'
else:
# Given 'build_config.release-R51-8172.B.json',
# return branch name 'release-R51-8172.B'.
return match.group(1)
else:
return None
class CheckTemplateStage(generic_stages.BuilderStage):
"""Stage that checks template files from GE bucket.
This stage lists template files from GE bucket,
triggers config updates if necessary.
"""
def __init__(self, builder_run, **kwargs):
super(CheckTemplateStage, self).__init__(builder_run, **kwargs)
self.ctx = gs.GSContext(init_boto=True)
def SortAndGetReleasePaths(self, release_list):
def _GetMilestone(file_name):
# Given 'build_config.release-R51-8172.B.json',
# search for milestone number '51'.
match = re.search(r'build_config\.release-R(.+?)-.+?\.json',
os.path.basename(file_name))
if match:
return int(match.group(1))
return None
milestone_path_pairs = []
for release_template in release_list:
milestone_num = _GetMilestone(release_template)
# Enable config-updater builder for master branch
# and release branches with milestone_num > 53
if milestone_num and milestone_num > 53:
milestone_path_pairs.append((milestone_num, release_template))
milestone_path_pairs.sort(reverse=True)
if len(release_list) <= 3:
return [i[1] for i in milestone_path_pairs]
else:
return [i[1] for i in milestone_path_pairs[0: 3]]
def _ListTemplates(self):
template_gs_paths = []
tot_gs_path = self.ctx.LS(GS_GE_TEMPLATE_TOT)
if tot_gs_path:
template_gs_paths.extend(tot_gs_path)
release_gs_paths = self.SortAndGetReleasePaths(
self.ctx.LS(GS_GE_TEMPLATE_RELEASE))
if release_gs_paths:
template_gs_paths.extend(release_gs_paths)
return template_gs_paths
def PerformStage(self):
template_gs_paths = self._ListTemplates()
if not template_gs_paths:
logging.info('No template files found. No need to update configs.')
return
chromite_dir = CreateTmpGitRepo('chromite', constants.CHROMITE_URL)
successful = True
failed_templates = []
for template_gs_path in template_gs_paths:
try:
branch = GetBranchName(os.path.basename(template_gs_path))
if branch:
UpdateConfigStage(self._run, template_gs_path, branch,
chromite_dir, suffix='_' + branch).Run()
except Exception as e:
successful = False
failed_templates.append(template_gs_path)
logging.error('Failed to update configs for %s: %s',
template_gs_path, e)
traceback.print_exc()
# If UpdateConfigStage failures happened, raise a exception
if not successful:
raise UpdateConfigException('Failed to update config for %s' %
failed_templates)
class UpdateConfigStage(generic_stages.BuilderStage):
"""Stage that verifies and updates configs.
This stage gets the template file from GE bucket,
checkout the corresponding branch, generates configs
based on the new template file, verifies the changes,
and submits the changes to the corresponding branch.
"""
CONFIG_DUMP_PATH = os.path.join('cbuildbot', 'config_dump.json')
WATERFALL_DUMP_PATH = os.path.join('cbuildbot', 'waterfall_layout_dump.txt')
GE_CONFIG_LOCAL_PATH = os.path.join('cbuildbot', GE_BUILD_CONFIG_FILE)
def __init__(self, builder_run, template_gs_path,
branch, chromite_dir, **kwargs):
super(UpdateConfigStage, self).__init__(builder_run, **kwargs)
self.template_gs_path = template_gs_path
self.chromite_dir = chromite_dir
self.branch = branch
self.ctx = gs.GSContext(init_boto=True)
self.dryrun = self._run.options.debug
def _CheckoutBranch(self):
"""Checkout to the corresponding branch in the temp repository.
Raises:
BranchNotFoundException if failed to checkout to the branch.
"""
git.RunGit(self.chromite_dir, ['checkout', self.branch])
output = git.RunGit(self.chromite_dir,
['rev-parse', '--abbrev-ref', 'HEAD']).output
current_branch = output.rstrip()
if current_branch != self.branch:
raise BranchNotFoundException(
"Failed to checkout to branch %s." % self.branch)
def _DownloadTemplate(self):
"""Download the template file from gs."""
dest_path = os.path.join(self.chromite_dir, 'cbuildbot',
GE_BUILD_CONFIG_FILE)
self.ctx.Copy(self.template_gs_path, dest_path)
def _ContainsUpdates(self, mod_files):
"""Check if updates exist and requires a push.
Args:
mod_files: files to check updates.
Returns:
True if updates exist; otherwise False.
"""
modifications = git.RunGit(self.chromite_dir,
['status', '--porcelain', '--'] + mod_files,
capture_output=True, print_cmd=True).output
if modifications:
logging.info('Changed files: %s ' % modifications)
return True
else:
return False
def _UpdateConfigDump(self):
"""Generate and dump configs base on the new template_file"""
# Clear the cached SiteConfig, if there was one.
config_lib.ClearConfigCache()
view_config_path = os.path.join(
self.chromite_dir, 'bin', 'cbuildbot_view_config')
cmd = [view_config_path, '--update']
cros_build_lib.RunCommand(cmd, cwd=os.path.dirname(self.chromite_dir))
show_waterfall_path = os.path.join(
self.chromite_dir, 'bin', 'cros_show_waterfall_layout')
cmd = [show_waterfall_path]
layout_file_name = os.path.join(
self.chromite_dir, 'cbuildbot/waterfall_layout_dump.txt')
with cros_build_lib.OutputCapturer(stdout_path=layout_file_name) as output:
cros_build_lib.RunCommand(cmd, cwd=os.path.dirname(self.chromite_dir))
logging.info("waterfall_layout_dump %s" % output.GetStdout())
def _RunUnitTest(self):
"""Run chromeos_config_unittest on top of the changes."""
logging.debug("Running chromeos_config_unittest")
rel_path = os.path.join('..', '..', 'chroot', GetWorkDir('chromite'))
unit_test_paths = [os.path.join(rel_path, 'chromite', 'cbuildbot',
'chromeos_config_unittest')]
cmd = ['cros_sdk', '--'] + unit_test_paths
cros_build_lib.RunCommand(cmd, cwd=os.path.dirname(self.chromite_dir))
def _PushCommits(self, candidate_files):
"""Commit and push changes to current branch.
Args:
candidate_files: candidate files to commit and update.
"""
git.RunGit(self.chromite_dir, ['add'] + candidate_files)
commit_msg = "Update config settings by config-updater."
git.RunGit(self.chromite_dir,
['commit', '-m', commit_msg],
print_cmd=True)
git.RunGit(self.chromite_dir, ['config', 'push.default', 'tracking'])
git.PushWithRetry(self.branch, self.chromite_dir, dryrun=self.dryrun)
def PerformStage(self):
logging.info('Update configs for branch %s, template gs path %s',
self.branch, self.template_gs_path)
candidate_files = [self.GE_CONFIG_LOCAL_PATH,
self.CONFIG_DUMP_PATH,
self.WATERFALL_DUMP_PATH]
try:
self._CheckoutBranch()
self._DownloadTemplate()
self._UpdateConfigDump()
self._RunUnitTest()
if self._ContainsUpdates(candidate_files):
self._PushCommits(candidate_files)
else:
logging.info('Nothing changed. No need to update configs for %s',
self.template_gs_path)
finally:
git.CleanAndDetachHead(self.chromite_dir)