| # 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. |
| |
| """Module that handles interactions with a Validation Pool. |
| |
| The validation pool is the set of commits that are ready to be validated i.e. |
| ready for the commit queue to try. |
| """ |
| |
| import json |
| import logging |
| import time |
| import urllib |
| from xml.dom import minidom |
| |
| from chromite.buildbot import gerrit_helper |
| from chromite.buildbot import lkgm_manager |
| from chromite.buildbot import patch as cros_patch |
| from chromite.lib import cros_build_lib |
| |
| |
| _BUILD_DASHBOARD = 'http://build.chromium.org/p/chromiumos' |
| _BUILD_INT_DASHBOARD = 'http://chromeos-botmaster.mtv.corp.google.com:8026' |
| |
| |
| class TreeIsClosedException(Exception): |
| """Raised when the tree is closed and we wanted to submit changes.""" |
| |
| def __init__(self): |
| super(TreeIsClosedException, self).__init__( |
| 'TREE IS CLOSED. PLEASE SET TO OPEN OR THROTTLED TO COMMIT') |
| |
| |
| class ValidationPool(object): |
| """Class that handles interactions with a validation pool. |
| |
| This class can be used to acquire a set of commits that form a pool of |
| commits ready to be validated and committed. |
| |
| Usage: Use ValidationPoo.AcquirePool -- a static |
| method that grabs the commits that are ready for validation. |
| """ |
| |
| GLOBAL_DRYRUN = False |
| |
| def __init__(self, internal, build_number, dryrun): |
| """Initializes an instance by setting default valuables to instance vars. |
| |
| Generally use AcquirePool as an entry pool to a pool rather than this |
| method. |
| |
| Args: |
| internal: Set to True if this is an internal validation pool. |
| build_number: Build number for this validation attempt. |
| dryrun: If set to True, do not submit anything to Gerrit. |
| """ |
| # TODO(sosa): Make more generic. |
| if not internal: |
| self.build_log = (_BUILD_DASHBOARD + |
| '/builders/x86%20generic%20commit%20queue/builds/' + |
| str(build_number)) |
| else: |
| self.build_log = (_BUILD_INT_DASHBOARD + |
| '/builders/x86%20mario%20commit%20queue/builds/' + |
| str(build_number)) |
| |
| self.changes = [] |
| self.changes_that_failed_to_apply_earlier = [] |
| self.gerrit_helper = None |
| self.dryrun = dryrun | self.GLOBAL_DRYRUN |
| |
| @classmethod |
| def _IsTreeOpen(cls): |
| """Returns True if the tree is open or throttled.""" |
| |
| def _SleepWithExponentialBackOff(current_sleep): |
| """Helper function to sleep with exponential backoff.""" |
| time.sleep(current_sleep) |
| return current_sleep * 2 |
| |
| max_attempts = 5 |
| response_dict = None |
| status_url = 'https://chromiumos-status.appspot.com/current?format=json' |
| current_sleep = 1 |
| |
| for attempt in range(max_attempts): |
| try: |
| response = urllib.urlopen(status_url) |
| # Check for valid response code. |
| if response.getcode() == 200: |
| response_dict = json.load(response) |
| break |
| |
| current_sleep = _SleepWithExponentialBackOff(current_sleep) |
| except IOError: |
| # We continue if we can't reach appspot.com |
| current_sleep = _SleepWithExponentialBackOff(current_sleep) |
| |
| else: |
| # We go ahead and say the tree is open if we can't tree the status page. |
| logging.warn('Could not get a status from %s', status_url) |
| return True |
| |
| if attempt > 1: |
| logging.warn('Had to attempt to reach %s %d times', status_url, attempt) |
| |
| tree_open = response_dict['general_state'] in ['open', 'throttled'] |
| return tree_open |
| |
| @classmethod |
| def AcquirePool(cls, branch, internal, buildroot, build_number, dryrun): |
| """Acquires the current pool from Gerrit. |
| |
| Polls Gerrit and checks for which change's are ready to be committed. |
| |
| Args: |
| branch: The branch for the validation pool. |
| internal: If True, use gerrit-int. |
| buildroot: The location of the buildroot used to filter projects. |
| build_number: Corresponding build number for the build. |
| dryrun: Don't submit anything to gerrit. |
| Returns: |
| ValidationPool object. |
| Raises: |
| TreeIsClosedException: if the tree is closed. |
| """ |
| if cls._IsTreeOpen(): |
| pool = ValidationPool(internal, build_number, dryrun) |
| pool.gerrit_helper = gerrit_helper.GerritHelper(internal) |
| raw_changes = pool.gerrit_helper.GrabChangesReadyForCommit(branch) |
| pool.changes = pool.gerrit_helper.FilterProjectsNotInSourceTree( |
| raw_changes, buildroot) |
| return pool |
| else: |
| raise TreeIsClosedException() |
| |
| @classmethod |
| def AcquirePoolFromManifest(cls, manifest, internal, build_number): |
| """Acquires the current pool from a given manifest. |
| |
| Args: |
| manifest: path to the manifest where the pool resides. |
| internal: if true, assume gerrit-int. |
| build_number: Corresponding build number for the build. |
| Returns: |
| ValidationPool object. |
| """ |
| pool = ValidationPool(internal, build_number, False) |
| pool.gerrit_helper = gerrit_helper.GerritHelper(internal) |
| manifest_dom = minidom.parse(manifest) |
| pending_commits = manifest_dom.getElementsByTagName( |
| lkgm_manager.PALADIN_COMMIT_ELEMENT) |
| for pending_commit in pending_commits: |
| project = pending_commit.getAttribute(lkgm_manager.PALADIN_PROJECT_ATTR) |
| change = pending_commit.getAttribute(lkgm_manager.PALADIN_CHANGE_ID_ATTR) |
| commit = pending_commit.getAttribute(lkgm_manager.PALADIN_COMMIT_ATTR) |
| pool.changes.append(pool.gerrit_helper.GrabPatchFromGerrit( |
| project, change, commit)) |
| |
| return pool |
| |
| def ApplyPoolIntoRepo(self, directory): |
| """Applies changes from pool into the repository. |
| |
| This method applies changes in the order specified. It also respects |
| dependency order. |
| |
| Returns: |
| True if we managed to apply some changes. |
| """ |
| changes_that_failed_to_apply_against_other_changes = set() |
| changes_that_failed_to_apply_to_tot = set() |
| changes_applied = set() |
| |
| # Change map maps Change-Id to GerritPatch object for lookup of dependent |
| # change-ids. |
| change_map = dict((change.revision, change) for change in self.changes) |
| for change in self.changes: |
| logging.debug('Trying change %s', change.revision) |
| # We've already attempted this change because it was a dependent change |
| # of another change that was ready. |
| if (change in changes_that_failed_to_apply_to_tot or |
| change in changes_applied): |
| continue |
| |
| # Change stacks consists of the change plus its dependencies in the order |
| # that they should be applied. |
| change_stack = [change] |
| deps = change.GerritDependencies(directory) |
| |
| # Put the dependent changes on the stack. |
| apply_chain = True |
| for dep in deps: |
| dep_change = change_map.get(dep) |
| if not dep_change: |
| # The dep may have been committed already. |
| if self.gerrit_helper.IsRevisionCommitted(change.project, dep): |
| logging.info('Dependency %s already submitted.', dep) |
| else: |
| logging.info('Cannot apply change %s because dependent change %s ' |
| 'is not ready to be committed.', change, dep) |
| apply_chain = False |
| break |
| else: |
| change_stack.insert(0, dep_change) |
| |
| # Should we apply the chain -- i.e. all deps are ready. |
| if not apply_chain: |
| continue |
| |
| # Apply changes in change_stack. For chains that were aborted early, |
| # we still want to apply changes in change_stack because they were |
| # ready to be committed (o/w wouldn't have been in the change_map). |
| for change in change_stack: |
| try: |
| if change in changes_applied: |
| continue |
| if change in changes_that_failed_to_apply_to_tot: |
| break |
| |
| change.Apply(directory, trivial=True) |
| changes_applied.add(change) |
| except cros_patch.ApplyPatchException as e: |
| if e.type == cros_patch.ApplyPatchException.TYPE_REBASE_TO_TOT: |
| changes_that_failed_to_apply_to_tot.add(change) |
| else: |
| changes_that_failed_to_apply_against_other_changes.add(change) |
| |
| break |
| else: |
| lkgm_manager.PrintLink(str(change), change.url) |
| |
| if changes_that_failed_to_apply_to_tot: |
| logging.debug('Some changes could not be applied cleanly.') |
| self.HandleApplicationFailure(changes_that_failed_to_apply_to_tot) |
| |
| self.changes = changes_applied |
| self.changes_that_failed_to_apply_earlier = list( |
| changes_that_failed_to_apply_against_other_changes) |
| return len(self.changes) > 0 |
| |
| def SubmitPool(self): |
| """Commits changes to Gerrit from Pool. |
| |
| Raises: |
| TreeIsClosedException: if the tree is closed. |
| """ |
| if ValidationPool._IsTreeOpen(): |
| for change in self.changes: |
| logging.info('Change %s will be submitted', change) |
| try: |
| change.Submit(self.gerrit_helper, dryrun=self.dryrun) |
| except cros_build_lib.RunCommandError: |
| change.HandleCouldNotSubmit(self.gerrit_helper, self.build_log, |
| dryrun=self.dryrun) |
| # TODO(sosa): Do we re-raise? |
| if self.changes_that_failed_to_apply_earlier: |
| self.HandleApplicationFailure(self.changes_that_failed_to_apply_earlier) |
| |
| else: |
| raise TreeIsClosedException() |
| |
| def HandleApplicationFailure(self, changes): |
| """Handles changes that were not able to be applied cleanly.""" |
| for change in changes: |
| logging.info('Change %s did not apply cleanly.', change) |
| change.HandleCouldNotApply(self.gerrit_helper, self.build_log, |
| dryrun=self.dryrun) |
| |
| def HandleValidationFailure(self): |
| """Handles failed changes by removing them from next Validation Pools.""" |
| logging.info('Validation failed for all changes.') |
| for change in self.changes: |
| logging.info('Validation failed for change %s.', change) |
| change.HandleCouldNotVerify(self.gerrit_helper, self.build_log, |
| dryrun=self.dryrun) |
| |