blob: b1594022a82f1faf4689c9c1178618ef79be4038 [file] [log] [blame]
# -*- coding: utf-8 -*-
# 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 errno
import os
import re
import textwrap
import traceback
from chromite.cbuildbot import repository
from chromite.cbuildbot.stages import generic_stages
from chromite.cbuildbot.stages import test_stages
from chromite.lib import constants
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
from chromite.lib import path_util
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."""
class ConfigNotFoundException(Exception):
"""Didn't find existing config files in branch."""
def GetProjectTmpDir(project):
"""Return the project tmp directory inside chroot.
Args:
project: The name of the project to create tmp dir.
"""
return os.path.join('tmp', 'tmp_%s' % project)
def GetProjectWorkDir(project):
"""Return the project work directory.
Args:
project: The name of the project to create work dir.
"""
project_work_dir = GetProjectTmpDir(project)
if not cros_build_lib.IsInsideChroot():
project_work_dir = os.path.join(
constants.SOURCE_ROOT, constants.DEFAULT_CHROOT_DIR, project_work_dir)
return project_work_dir
def GetProjectRepoDir(project, project_url, clean_old_dir=False):
"""Clone the project repo locally and return the repo directory.
Args:
project: git project name to clone.
project_url: git project url to clone.
clean_old_dir: Boolean to indicate whether to clean old work_dir. Default
to False.
Returns:
project_dir: local project directory.
"""
work_dir = GetProjectWorkDir(project)
if clean_old_dir:
# Delete the work_dir built by previous runs.
osutils.RmDir(work_dir, ignore_missing=True, sudo=True)
osutils.SafeMakedirs(work_dir)
project_dir = os.path.join(work_dir, project)
if not os.path.exists(project_dir):
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.
"""
category = constants.CI_INFRA_STAGE
def __init__(self, builder_run, buildstore, **kwargs):
super(CheckTemplateStage, self).__init__(builder_run, buildstore, **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):
"""List and return template files from GS bucket.
Returns:
A list of template files.
"""
template_gs_paths = []
try:
tot_gs_path = self.ctx.LS(GS_GE_TEMPLATE_TOT)
if tot_gs_path:
template_gs_paths.extend(tot_gs_path)
except gs.GSNoSuchKey as e:
logging.warning('No matching objects for %s: %s', GS_GE_TEMPLATE_TOT, e)
try:
release_gs_paths = self.SortAndGetReleasePaths(
self.ctx.LS(GS_GE_TEMPLATE_RELEASE))
if release_gs_paths:
template_gs_paths.extend(release_gs_paths)
except gs.GSNoSuchKey as e:
logging.warning('No matching objects for %s: %s', GS_GE_TEMPLATE_RELEASE,
e)
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 = GetProjectRepoDir(
'chromite', constants.CHROMITE_URL, clean_old_dir=True)
successful = True
failed_templates = []
for template_gs_path in template_gs_paths:
try:
branch = GetBranchName(os.path.basename(template_gs_path))
UpdateConfigStage(
self._run,
self.buildstore,
template_gs_path,
branch,
chromite_dir,
self._run.options.debug,
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.
"""
category = constants.CI_INFRA_STAGE
def __init__(self, builder_run, buildstore, template_gs_path, branch,
chromite_dir, dry_run, **kwargs):
super(UpdateConfigStage, self).__init__(builder_run, buildstore, **kwargs)
self.template_gs_path = template_gs_path
self.chromite_dir = chromite_dir
self.branch = branch
self.ctx = gs.GSContext(init_boto=True)
self.dry_run = dry_run
# Filled in by _SetupConfigPaths, will cause errors if not filled in.
self.config_dir = None
self.config_paths = None
self.ge_config_local_path = None
def _CheckoutBranch(self):
"""Checkout to the corresponding branch in the temp repository.
Raises:
BranchNotFoundException if failed to checkout to the branch.
"""
logging.info('Checking out %s in %s', self.branch, self.chromite_dir)
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 _SetupConfigPaths(self):
"""These config files can move based on the branch.
Detect and save off the paths to them for the current path.
"""
# These are the two directories inside cbuildbot where these files can
# exist, and order of preference.
dirs = ('config', 'cbuildbot')
files = (GE_BUILD_CONFIG_FILE, 'config_dump.json',
'waterfall_layout_dump.txt')
for d in dirs:
self.config_dir = d
self.config_paths = [os.path.join(self.chromite_dir, d, f) for f in files]
self.ge_config_local_path = self.config_paths[0]
if os.path.exists(self.ge_config_local_path):
logging.info('Found config in %s', self.config_dir)
break
else:
raise ConfigNotFoundException(
'Failed to find configs in branch %s.' % self.branch)
def _DownloadTemplate(self):
"""Download the template file from gs."""
self.ctx.Copy(self.template_gs_path, self.ge_config_local_path)
def _ContainsConfigUpdates(self):
"""Check if updates exist and requires a push.
Returns:
True if updates exist; otherwise False.
"""
modifications = git.RunGit(
self.chromite_dir, ['status', '--porcelain', '--'] + self.config_paths,
capture_output=True,
print_cmd=True).output
if modifications:
logging.info('Changed files: %s ', modifications)
return True
else:
return False
def _RunUnitTest(self):
"""Run chromeos_config_unittest on top of the changes."""
logging.debug('Running chromeos_config_unittest')
test_path = path_util.ToChrootPath(
os.path.join(self.chromite_dir, self.config_dir,
'chromeos_config_unittest'))
# Because of --update, this updates our generated files.
cmd = ['cros_sdk', '--', test_path, '--update']
cros_build_lib.run(cmd, cwd=os.path.dirname(self.chromite_dir))
def _CreateConfigPatch(self):
"""Create and return a diff patch file for config changes."""
config_change_patch = os.path.join(self.chromite_dir, 'config_change.patch')
try:
os.remove(config_change_patch)
except OSError as e:
if e.errno != errno.ENOENT:
raise
result = git.RunGit(
self.chromite_dir, ['diff'] + self.config_paths, print_cmd=True)
with open(config_change_patch, 'w') as f:
f.write(result.output)
return config_change_patch
def _RunBinhostTest(self):
"""Run BinhostTest stage for master branch."""
config_change_patch = self._CreateConfigPatch()
# Apply config patch.
git.RunGit(
constants.CHROMITE_DIR, ['apply', config_change_patch], print_cmd=True)
test_stages.BinhostTestStage(
self._run, self.buildstore, suffix='_' + self.branch).Run()
# Clean config patch.
git.RunGit(constants.CHROMITE_DIR, ['checkout', '.'], print_cmd=True)
def _PushCommits(self):
"""Commit and push changes to current branch."""
git.RunGit(self.chromite_dir, ['add'] + self.config_paths, print_cmd=True)
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'],
print_cmd=True)
git.PushBranch(self.branch, self.chromite_dir, dryrun=self.dry_run)
def PerformStage(self):
logging.info('Update configs for branch %s, template gs path %s',
self.branch, self.template_gs_path)
try:
self._CheckoutBranch()
self._SetupConfigPaths()
self._DownloadTemplate()
self._RunUnitTest()
if self._ContainsConfigUpdates():
if self.branch == 'master':
self._RunBinhostTest()
self._PushCommits()
else:
logging.info('Nothing changed. No need to update configs for %s',
self.template_gs_path)
finally:
git.CleanAndDetachHead(self.chromite_dir)
class DeployLuciSchedulerStage(generic_stages.BuilderStage):
"""Stage that deploys updates to luci_scheduler.cfg.
We autogenerate luci_scheduler.cfg, and submit that file into chromite
for review purposes. However, it must be submitted into the LUCI Project
config "chromeos" to be deployed. This stage autodeploys scheduler changes
from chromite.
"""
category = constants.CI_INFRA_STAGE
PROJECT_URL = os.path.join(constants.INTERNAL_GOB_URL,
'chromeos/infra/config')
PROJECT_BRANCH = 'master'
def __init__(self, builder_run, buildstore, **kwargs):
super(DeployLuciSchedulerStage, self).__init__(builder_run, buildstore,
**kwargs)
self.legacy_project_dir = None
self.project_dir = None
def _RunUnitTest(self):
"""Run chromeos_config_unittest to confirm a clean scheduler config."""
logging.debug('Running chromeos_config_unittest, to confirm sane state.')
test_path = path_util.ToChrootPath(
os.path.join(constants.CHROMITE_DIR, 'config',
'chromeos_config_unittest'))
cmd = ['cros_sdk', '--', test_path]
cros_build_lib.run(cmd, cwd=constants.CHROMITE_DIR)
def _MakeWorkDir(self, name):
"""Makes and returns the path to a temporary directory.
Args:
name: name to use in the creation of the temporary directory.
"""
path = GetProjectWorkDir(name)
osutils.RmDir(path, ignore_missing=True, sudo=True)
osutils.SafeMakedirs(path)
return path
def _CheckoutLuciProject(self):
"""Checkout the LUCI project config.
Raises:
BranchNotFoundException if failed to checkout to the branch.
"""
self.project_dir = self._MakeWorkDir('luci_config')
git.Clone(self.project_dir, self.PROJECT_URL, branch=self.PROJECT_BRANCH)
logging.info('Checked out luci config %s:%s in %s',
self.PROJECT_URL, self.PROJECT_BRANCH, self.project_dir)
def _UpdateLuciProject(self):
chromite_source_file = os.path.join(constants.CHROMITE_DIR, 'config',
'luci-scheduler.cfg')
generated_source_file = os.path.join(self.project_dir, 'generated',
'luci-scheduler.cfg')
target_file = os.path.join(self.project_dir, 'luci', 'luci-scheduler.cfg')
concatenated_content = (osutils.ReadFile(chromite_source_file) + '\n\n'
+ osutils.ReadFile(generated_source_file))
if concatenated_content == osutils.ReadFile(target_file):
logging.PrintBuildbotStepText(
'luci-scheduler.cfg current: No Update.')
return
chromite_rev = git.RunGit(
constants.CHROMITE_DIR,
['rev-parse', 'HEAD:config/luci-scheduler.cfg']).output.rstrip()
message = textwrap.dedent("""\
luci-scheduler.cfg: Chromite %s
Auto update to match generated file in chromite and luci config.
""" % chromite_rev)
with open(target_file, 'w') as f:
f.write(concatenated_content)
git.RunGit(self.project_dir, ['add', '-A'])
git.RunGit(self.project_dir, ['commit', '-m', message])
push_to = git.RemoteRef('origin', self.PROJECT_BRANCH)
logging.info('Pushing to branch (%s) with message: %s %s', push_to, message,
' (dryrun)' if self._run.options.debug else '')
git.RunGit(
self.project_dir, ['config', 'push.default', 'tracking'],
print_cmd=True)
git.PushBranch(self.PROJECT_BRANCH, self.project_dir,
dryrun=self._run.options.debug)
logging.PrintBuildbotStepText('luci-scheduler.cfg: Updated.')
def PerformStage(self):
"""Perform the DeployLuciSchedulerStage."""
logging.info('Update luci_scheduler.cfg at %s:%s.',
self.PROJECT_URL, self.PROJECT_BRANCH)
self._RunUnitTest()
self._CheckoutLuciProject()
self._UpdateLuciProject()