| # -*- 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 traceback |
| |
| from chromite.cbuildbot import repository |
| from chromite.cbuildbot.stages import generic_stages |
| from chromite.cbuildbot.stages import test_stages |
| from chromite.lib import config_lib |
| 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 |
| |
| |
| 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 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. |
| """ |
| |
| 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): |
| """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)) |
| if branch: |
| UpdateConfigStage(self._run, 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. |
| """ |
| |
| 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) |
| |
| CONFIG_PATHS = (GE_CONFIG_LOCAL_PATH, |
| CONFIG_DUMP_PATH, |
| WATERFALL_DUMP_PATH) |
| |
| def __init__(self, builder_run, template_gs_path, |
| branch, chromite_dir, dry_run, **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.dry_run = dry_run |
| |
| 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 _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', '--'] + list(self.CONFIG_PATHS), |
| 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'] |
| |
| try: |
| cros_build_lib.RunCommand(cmd, cwd=os.path.dirname(self.chromite_dir)) |
| except: |
| logging.error('Failed to update configs. Please check the format of the ' |
| 'remote template file %s and the local template copy %s', |
| self.template_gs_path, self.CONFIG_DUMP_PATH) |
| raise |
| |
| 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): |
| cros_build_lib.RunCommand(cmd, cwd=os.path.dirname(self.chromite_dir)) |
| |
| def _RunUnitTest(self): |
| """Run chromeos_config_unittest on top of the changes.""" |
| logging.debug("Running chromeos_config_unittest") |
| rel_path = os.path.join('..', '..', 'chroot', GetProjectTmpDir('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 _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'] + list(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, suffix='_' + self.branch).Run() |
| |
| # Clean config patch. |
| git.RunGit(constants.CHROMITE_DIR, ['checkout'] + list(self.CONFIG_PATHS), |
| print_cmd=True) |
| |
| def _PushCommits(self): |
| """Commit and push changes to current branch.""" |
| git.RunGit(self.chromite_dir, ['add'] + list(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.PushWithRetry(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._DownloadTemplate() |
| self._UpdateConfigDump() |
| 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) |