<reponame>khromiumos/chromiumos-chromite<gh_stars>0
# -*- coding: utf-8 -*-
# Copyright (c) 2013 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 completion stages."""

from __future__ import print_function
from __future__ import division

from chromite.cbuildbot import commands
from chromite.cbuildbot import prebuilts
from chromite.cbuildbot.stages import generic_stages
from chromite.cbuildbot.stages import sync_stages
from chromite.lib import alerts
from chromite.lib import buildbucket_lib
from chromite.lib import builder_status_lib
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cros_logging as logging
from chromite.lib import failures_lib
from chromite.lib import portage_util


def GetBuilderSuccessMap(builder_run, overall_success):
  """Get the pass/fail status of all builders.

  A builder is marked as passed if all of its steps ran all of the way to
  completion. We determine this by looking at whether all of the steps for
  all of the constituent boards ran to completion.

  In cases where a builder does not have any boards, or has child boards, we
  fall back and instead just look at whether the entire build was successful.

  Args:
    builder_run: The builder run we wish to get the status of.
    overall_success: The overall status of the build.

  Returns:
    A dict, mapping the builder names to whether they succeeded.
  """
  success_map = {}
  for run in [builder_run] + builder_run.GetChildren():
    if run.config.boards and not run.config.child_configs:
      success_map[run.config.name] = True
      for board in run.config.boards:
        board_runattrs = run.GetBoardRunAttrs(board)
        if not board_runattrs.HasParallel('success'):
          success_map[run.config.name] = False
    else:
      # If a builder does not have boards, or if it has child configs, we
      # will just use the overall status instead.
      success_map[run.config.name] = overall_success
  return success_map


class ManifestVersionedSyncCompletionStage(
    generic_stages.ForgivingBuilderStage):
  """Stage that records board specific results for a unique manifest file."""

  option_name = 'sync'
  category = constants.CI_INFRA_STAGE

  def __init__(self, builder_run, buildstore, sync_stage, success, **kwargs):
    super(ManifestVersionedSyncCompletionStage, self).__init__(
        builder_run, buildstore, **kwargs)
    self.sync_stage = sync_stage
    self.success = success
    # Message that can be set that well be sent along with the status in
    # UpdateStatus.
    self.message = None

  def PerformStage(self):
    if not self.success:
      self.message = self.GetBuildFailureMessage()

    if not config_lib.IsPFQType(self._run.config.build_type):
      # Update the pass/fail status in the manifest-versions
      # repo. Suite scheduler checks the build status to schedule
      # suites.
      self._run.attrs.manifest_manager.UpdateStatus(
          success_map=GetBuilderSuccessMap(self._run, self.success),
          message=self.message)


class ImportantBuilderFailedException(failures_lib.StepFailure):
  """Exception thrown when an important build fails to build."""


class MasterSlaveSyncCompletionStage(ManifestVersionedSyncCompletionStage):
  """Stage that records whether we passed or failed to build/test manifest."""

  category = constants.CI_INFRA_STAGE

  def __init__(self, *args, **kwargs):
    super(MasterSlaveSyncCompletionStage, self).__init__(*args, **kwargs)
    # TODO(nxia): rename to _build_statuses as it contains local status and
    # slave statuses for master builds
    self._slave_statuses = {}
    self._experimental_build_statuses = {}
    self._fatal = False
    self.buildbucket_client = self.GetBuildbucketClient()

  def _WaitForSlavesToComplete(self, manager, build_identifier, builders_array,
                               timeout):
    """Wait for slave builds to complete.

    Args:
      manager: An instance of BuildSpecsManager.
      build_identifier: The BuildIdentifier instance of the master build.
      builders_array: A list of builder names (strings) of slave builds.
      timeout: Number of seconds to wait for the results.
    """
    return manager.WaitForSlavesToComplete(
        build_identifier, builders_array, timeout=timeout)

  def _GetBuilderStatusesFetcher(self):
    """Construct and return the BuilderStatusesFetcher instance.

    If this build is a master, wait for slaves to complete or timeout before
    constructing and returning the BuilderStatusesFetcher instance.

    Returns:
      A instance of builder_status_lib.BuilderStatusesFetcher.
    """
    # Wait for slaves if we're a master, in production or mock-production.
    # Otherwise just look at our own status.
    build_identifier, _ = self._run.GetCIDBHandle()
    builders_array = None
    if not self._run.config.master:
      # The slave build returns its own status.
      logging.warning('The build is not a master.')
    elif self._run.options.mock_slave_status or not self._run.options.debug:
      # The master build.
      builders = self._GetSlaveConfigs()
      builders_array = [b.name for b in builders]
      timeout = self._run.config.build_timeout

      if self._run.options.debug:
        # For debug runs, wait for three minutes to ensure most code
        # paths are executed.
        logging.info(
            'Waiting for 3 minutes only for debug run. '
            'Would have waited for %s seconds.', timeout)
        timeout = 3 * 60

      manager = self._run.attrs.manifest_manager
      if sync_stages.MasterSlaveLKGMSyncStage.external_manager:
        manager = sync_stages.MasterSlaveLKGMSyncStage.external_manager

      self._WaitForSlavesToComplete(manager, build_identifier, builders_array,
                                    timeout)

    # Set exclude_experimental to False to fetch the BuilderStatus for
    # builds which are important in config but marked as experimental in
    # the tree status.
    builder_statuses_fetcher = builder_status_lib.BuilderStatusesFetcher(
        build_identifier,
        self.buildstore,
        self.success,
        self.message,
        self._run.config,
        self._run.attrs.metadata,
        self.buildbucket_client,
        builders_array=builders_array,
        exclude_experimental=False,
        dry_run=self._run.options.debug)

    return builder_statuses_fetcher

  def _HandleStageException(self, exc_info):
    """Decide whether an exception should be treated as fatal."""
    # Besides the master, the completion stages also run on slaves, to report
    # their status back to the master. If the build failed, they throw an
    # exception here. For slave builders, marking this stage 'red' would be
    # redundant, since the build itself would already be red. In this case,
    # report a warning instead.
    # pylint: disable=protected-access
    exc_type = exc_info[0]
    if (issubclass(exc_type, ImportantBuilderFailedException) and
        not self._run.config.master):
      return self._HandleExceptionAsWarning(exc_info)
    else:
      # In all other cases, exceptions should be treated as fatal. To
      # implement this, we bypass ForgivingStage and call
      # generic_stages.BuilderStage._HandleStageException explicitly.
      return generic_stages.BuilderStage._HandleStageException(self, exc_info)

  def HandleSuccess(self):
    """Handle a successful build.

    This function is called whenever the cbuildbot run is successful.
    For the master, this will only be called when all slave builders
    are also successful. This function may be overridden by subclasses.
    """
    # We only promote for the pfq, not chrome pfq.
    # TODO(build): Run this logic in debug mode too.
    if (not self._run.options.debug and
        config_lib.IsPFQType(self._run.config.build_type) and
        self._run.config.master and self._run.manifest_branch == 'master'):
      self._run.attrs.manifest_manager.PromoteCandidate()
      if sync_stages.MasterSlaveLKGMSyncStage.external_manager:
        sync_stages.MasterSlaveLKGMSyncStage.external_manager.PromoteCandidate()

  def HandleFailure(self, failing, inflight, no_stat, self_destructed):
    """Handle a build failure.

    This function is called whenever the cbuildbot run fails.
    For the master, this will be called when any slave fails or times
    out. This function may be overridden by subclasses.

    Args:
      failing: The names of the failing builders.
      inflight: The names of the builders that are still running.
      no_stat: Set of builder names of slave builders that had status None.
      self_destructed: Boolean indicating whether the master build destructed
                       itself and stopped waiting completion of its slaves.
    """
    if failing or inflight or no_stat:
      logging.PrintBuildbotStepWarnings()

    if failing:
      logging.warning('\n'.join([
          'The following builders failed with this manifest:', ', '.join(
              sorted(failing)),
          'Please check the logs of the failing builders for details.'
      ]))

    if not self_destructed and inflight:
      logging.warning('\n'.join([
          'The following builders took too long to finish:', ', '.join(
              sorted(inflight)),
          'Please check the logs of these builders for details.'
      ]))

    if no_stat:
      logging.warning('\n'.join([
          'The following builders did not start or failed prematurely:',
          ', '.join(sorted(no_stat)),
          'Please check the logs of these builders for details.'
      ]))

  def PerformStage(self):
    super(MasterSlaveSyncCompletionStage, self).PerformStage()

    builder_statusess_fetcher = self._GetBuilderStatusesFetcher()
    self._slave_statuses, self._experimental_build_statuses = (
        builder_statusess_fetcher.GetBuilderStatuses())

    no_stat = builder_status_lib.BuilderStatusesFetcher.GetNostatBuilds(
        self._slave_statuses)
    failing = builder_status_lib.BuilderStatusesFetcher.GetFailingBuilds(
        self._slave_statuses)
    inflight = builder_status_lib.BuilderStatusesFetcher.GetInflightBuilds(
        self._slave_statuses)

    self_destructed = self._run.attrs.metadata.GetValueWithDefault(
        constants.SELF_DESTRUCTED_BUILD, False)

    self._fatal = self._IsFailureFatal(
        failing, inflight, no_stat, self_destructed=self_destructed)

    # Always annotate unsuccessful builders.
    self._AnnotateFailingBuilders(
        failing, inflight, no_stat, self._slave_statuses,
        self._experimental_build_statuses, self_destructed)

    if self._fatal:
      self.HandleFailure(failing, inflight, no_stat, self_destructed)
      raise ImportantBuilderFailedException()
    else:
      self.HandleSuccess()

  def _IsFailureFatal(self, failing, inflight, no_stat, self_destructed=False):
    """Returns a boolean indicating whether the build should fail.

    Args:
      failing: Set of build config names of builders that failed.
      inflight: Set of build config names of builders that are inflight
      no_stat: Set of build config names of builders that had status None.
      self_destructed: Boolean indicating whether it's a master build which
        destructed itself and stopped waiting its slaves to complete.

    Returns:
      True if any of the failing, inflight or no_stat builders are not sanity
      checker builders and not ignored by self-destruction; else, False.
    """
    not_passed_builders = failing | inflight | no_stat

    if self_destructed:
      # This build must be a master build if self_destructed is True.
      self_destructed_with_success = (
          self._run.attrs.metadata.GetValueWithDefault(
              constants.SELF_DESTRUCTED_WITH_SUCCESS_BUILD, False))
      if self_destructed_with_success:
        # If the master build itself didn't pass, report fatal.
        return self._run.config.name in not_passed_builders

      build_identifier, _ = self._run.GetCIDBHandle()
      if self.buildstore.AreClientsReady():
        aborted_slaves = (
            builder_status_lib.GetSlavesAbortedBySelfDestructedMaster(
                build_identifier, self.buildstore))
        # Ignore the slaves aborted by self-destruction.
        not_passed_builders -= aborted_slaves

    # Fatal if any not_passed_builders remain.
    return bool(not_passed_builders)

  def _PrintBuildMessage(self, text, url=None):
    """Print the build message.

    Args:
      text: Text (string) to print.
      url: URL (string) to link to the text, default to None.
    """
    if url is not None:
      logging.PrintBuildbotLink(text, url)
    else:
      logging.PrintBuildbotStepText(text)

  def _AnnotateNoStatBuilders(self, no_stat):
    """Annotate the build statuses fetched from the Buildbucket.

    Some builds may fail to upload statuses to GS. If the builds were
    scheduled by Buildbucket, get the build statuses and annotate the results.

    Args:
      no_stat: Config names of the slave builds with None status.
    """
    buildbucket_info_dict = buildbucket_lib.GetBuildInfoDict(
        self._run.attrs.metadata)

    for config_name in no_stat:
      if config_name in buildbucket_info_dict:
        buildbucket_id = buildbucket_info_dict[config_name].buildbucket_id
        assert buildbucket_id is not None, 'buildbucket_id is None'
        try:
          content = self.buildbucket_client.GetBuildRequest(
              buildbucket_id, self._run.options.debug)

          status = buildbucket_lib.GetBuildStatus(content)
          result = buildbucket_lib.GetBuildResult(content)

          text = '%s: [status] %s [result] %s' % (config_name, status, result)

          if result == constants.BUILDBUCKET_BUILDER_RESULT_FAILURE:
            failure_reason = buildbucket_lib.GetBuildFailureReason(content)
            if failure_reason:
              text += ' [failure_reason] %s' % failure_reason
          elif result == constants.BUILDBUCKET_BUILDER_RESULT_CANCELED:
            cancel_reason = buildbucket_lib.GetBuildCancelationReason(content)
            if cancel_reason:
              text += ' [cancelation_reason] %s' % cancel_reason

          dashboard_url = buildbucket_lib.GetBuildURL(content)
          if dashboard_url:
            logging.PrintBuildbotLink(text, dashboard_url)
          else:
            logging.PrintBuildbotStepText(text)
        except buildbucket_lib.BuildbucketResponseException as e:
          logging.error('Cannot get status for %s: %s', config_name, e)
          logging.PrintBuildbotStepText(
              'No status found for build %s buildbucket_id %s' %
              (config_name, buildbucket_id))
      else:
        logging.PrintBuildbotStepText(
            "%s wasn't scheduled by master." % config_name)

  def _AnnotateFailingBuilders(self, failing, inflight, no_stat, statuses,
                               experimental_statuses, self_destructed):
    """Annotate the failing, inflight and no_stat builds with text and links.

    Add text and buildbot links to build dashboards for failing builds and
    inflight builds. For master builds using Buildbucket schdeduler, add text
    and buildbot links for the no_stat builds; for other master builds, add
    step text for the no_stat builds.

    Args:
      failing: Set of builder names of slave builders that failed.
      inflight: Set of builder names of slave builders that are inflight.
      no_stat: Set of builder names of slave builders that had status None.
      statuses: A builder-name->status dictionary, which will provide
                the dashboard_url values for any links.
      experimental_statuses: A builder-name->status dictionary for all slaves
                             that were set as experimental through the tree
                             status.
      self_destructed: Boolean indicating whether the master build destructed
                       itself and stopped waiting completion of its slaves.
    """
    for build in failing:
      if statuses[build].message:
        self._PrintBuildMessage(
            '%s: %s' % (build, statuses[build].message.reason),
            statuses[build].dashboard_url)
      else:
        self._PrintBuildMessage('%s: failed due to unknown reasons' % build,
                                statuses[build].dashboard_url)

    if not self_destructed:
      for build in inflight:
        self._PrintBuildMessage('%s: timed out' % build,
                                statuses[build].dashboard_url)

      self._AnnotateNoStatBuilders(no_stat)
    else:
      logging.PrintBuildbotStepText('The master destructed itself and stopped '
                                    'waiting for the following slaves:')
      for build in inflight:
        self._PrintBuildMessage('%s: still running' % build,
                                statuses[build].dashboard_url)

      self._AnnotateNoStatBuilders(no_stat)

    for build, status in experimental_statuses.items():
      if not status.Passed():
        self._PrintBuildMessage(
            '%s: set as experimental through tree status' % build,
            status.dashboard_url)

  def GetSlaveStatuses(self):
    """Returns cached slave status results.

    Cached results are populated during PerformStage, so this function
    should only be called after PerformStage has returned.

    Returns:
      A dictionary from build names to builder_status_lib.BuilderStatus
      builder status objects.
    """
    return self._slave_statuses

  def GetExperimentalBuildStatuses(self):
    """Returns cached BuilderStatus of the experimental slave builds.

    Cached results are populated during PerformStage, so this function
    should only be called after PerformStage has returned.

    Returns:
      A dictionary from names of experimental slave builds to their
        builder_status_lib.BuilderStatus objects.
    """
    return self._experimental_build_statuses

  def GetFatal(self):
    """Returns whether the build (and its important slaves) failed with fatal.

    Cached results are populated during PerformStage, so this function
    should only be called after PerformStage has returned.

    Returns:
      A boolean indicating whether the build  (and its important slaves) failed
      with fatal.
    """
    return self._fatal


class CanaryCompletionStage(MasterSlaveSyncCompletionStage):
  """Collect build slave statuses and handle the failures."""

  category = constants.CI_INFRA_STAGE

  def HandleFailure(self, failing, inflight, no_stat, self_destructed):
    """Handle a build failure or timeout in the Canary builders.

    Args:
      failing: Names of the builders that failed.
      inflight: Names of the builders that timed out.
      no_stat: Set of builder names of slave builders that had status None.
      self_destructed: Boolean indicating whether the master build destructed
                       itself and stopped waiting completion of its slaves.
    """
    # Print out the status about what builds failed or not.
    MasterSlaveSyncCompletionStage.HandleFailure(self, failing, inflight,
                                                 no_stat, self_destructed)

    if self._run.config.master:
      self.CanaryMasterHandleFailure(failing, inflight, no_stat)

  def SendCanaryFailureAlert(self, failing, inflight, no_stat):
    """Send an alert email to summarize canary failures.

    Args:
      failing: The names of the failing builders.
      inflight: The names of the builders that are still running.
      no_stat: The names of the builders that had status None.
    """
    builder_name = '<NAME>'
    title = '%s has detected build failures:' % builder_name
    msgs = [
        str(x) for x in builder_status_lib.GetFailedMessages(
            self._slave_statuses, failing)
    ]
    slaves = builder_status_lib.GetBuildersWithNoneMessages(
        self._slave_statuses, failing)
    msgs += ['%s failed with unknown reason.' % x for x in slaves]
    msgs += ['%s timed out' % x for x in inflight]
    msgs += ['%s did not start' % x for x in no_stat]
    msgs.insert(0, title)
    msgs.append('You can also view the summary of the slave failures from '
                'the %s stage of %s. Click on the failure message to go '
                "to an individual slave's build status page: %s" %
                (self.name, builder_name, self.ConstructDashboardURL()))
    msg = '\n\n'.join(msgs)
    logging.warning(msg)
    extra_fields = {'X-cbuildbot-alert': 'canary-fail-alert'}
    alerts.SendHealthAlert(
        self._run, 'Canary builder failures', msg, extra_fields=extra_fields)

  def CanaryMasterHandleFailure(self, failing, inflight, no_stat):
    """Handles the failure by sending out an alert email.

    Args:
      failing: Names of the builders that failed.
      inflight: Names of the builders that timed out.
      no_stat: Set of builder names of slave builders that had status None.
    """
    if self._run.manifest_branch == 'master':
      self.SendCanaryFailureAlert(failing, inflight, no_stat)
      # Note: We used to throttle the tree here. As of
      # https://chromium-review.googlesource.com/#/c/325821/ we no longer do.

  def _HandleStageException(self, exc_info):
    """Decide whether an exception should be treated as fatal."""
    # Canary master already updates the tree status for slave
    # failures. There is no need to mark this stage red. For slave
    # builders, the build itself would already be red. In this case,
    # report a warning instead.
    # pylint: disable=protected-access
    exc_type = exc_info[0]
    if issubclass(exc_type, ImportantBuilderFailedException):
      return self._HandleExceptionAsWarning(exc_info)
    else:
      # In all other cases, exceptions should be treated as fatal.
      return super(CanaryCompletionStage, self)._HandleStageException(exc_info)


class UpdateChromeosLKGMStage(generic_stages.BuilderStage):
  """Update the CHROMEOS_LKGM file in the chromium repository."""

  category = constants.CI_INFRA_STAGE

  def PerformStage(self):
    if not self._build_threshold_successful():
      logging.info('Insufficient number of successful builders. '
                   'Skipping LKGM update.')
      return

    manager = self._run.attrs.manifest_manager
    cmd = ['chrome_chromeos_lkgm', '--lkgm=%s' % manager.current_version]
    # Always do a dryrun for now so that we can check the output and ensure it
    # is doing the correct thing.
    if self._run.options.debug:
      cmd.append('--dryrun')
    commands.RunBuildScript(self._build_root, cmd, chromite_cmd=True)

  def _build_threshold_successful(self):
    """Whether the percentage of successful child builders exceeds threshold"""
    ids = self.GetScheduledSlaveBuildbucketIds()
    num_builds = 0
    num_failures = 0
    for status in self.buildstore.GetBuildStatuses(buildbucket_ids=ids):
      if status.get('important'):
        num_builds += 1
        if status.get('status') != constants.BUILDER_STATUS_PASSED:
          num_failures += 1
    logging.info('%d of %d important builds failed.', num_failures, num_builds)
    if num_builds > 0:
      pct_succeeded = 100.0 * ((num_builds - num_failures) / float(num_builds))
      return pct_succeeded >= constants.LKGM_THRESHOLD
    return False

class PublishUprevChangesStage(generic_stages.BuilderStage):
  """Makes CQ uprev changes live for developers.

  Push local commits for uprevs, binhost, and portage cache. We resync to the
  latest version of repos as they exist in GoB.  We can't rely on the commits
  we pulled originally because our CL submit stage might have failed in some way
  (GoB sometimes flakes), or we don't want to submit all the CLs (we pushed some
  repos, but rejected others based on CQ repo settings). There might also be
  commits pushed independently (chumped by sheriffs or the precq submitted).
  """

  category = constants.CI_INFRA_STAGE

  def __init__(self,
               builder_run,
               buildstore,
               sync_stage,
               success,
               **kwargs):
    """Constructor.

    Args:
      builder_run: BuilderRun object.
      buildstore: BuildStore instance to make DB calls with.
      sync_stage: An instance of sync stage.
      success: Boolean indicating whether the build succeeded.
    """
    super(PublishUprevChangesStage, self).__init__(builder_run, buildstore,
                                                   **kwargs)
    self.sync_stage = sync_stage
    self.success = success

  def CheckMasterBinhostTest(self, buildbucket_id):
    """Check whether the master builder has passed BinhostTest stage.

    Args:
      buildbucket_id: buildbucket_id of the master build to check for.

    Returns:
      True if the status of the master build BinhostTest stage is 'pass';
      else, False.
    """
    stage_name = 'BinhostTest'

    if self._build_stage_id is not None and self.buildstore.AreClientsReady():
      stages = self.buildstore.GetBuildsStages(buildbucket_ids=[buildbucket_id])

      # No stages found. BinhostTest stage didn't start or got skipped,
      # in both case we don't need to push commits to the temp pfq branch.
      if not stages:
        logging.warning('no %s stage found in build %s',
                        stage_name, buildbucket_id)
        return False

      stage_status = [
          s for s in stages if (s['name'] == stage_name and
                                s['status'] == constants.BUILDER_STATUS_PASSED)
      ]
      if stage_status:
        logging.info('build %s passed stage %s with %s', buildbucket_id,
                     stage_name, stage_status)
        return True
      else:
        logging.warning('build %s stage %s result %s', buildbucket_id,
                        stage_name, stage_status)
        return False

    logging.warning('Not valid build_stage_id %s or no %s found',
                    self._build_stage_id, stage_name)
    return False

  def CheckSlaveUploadPrebuiltsTest(self):
    """Check if the slaves have passed UploadPrebuilts stage.

    Given the master build id, check if all the important slaves have passed
    the UploadPrebuilts stage.

    Returns:
      True if all the important slaves have passed the stage;
      True if it's in debug environment;
      else, False.
    """
    stage_name = 'UploadPrebuilts'

    if not self._run.config.master:
      logging.warning('The build is not a master')
      return False
    elif self._run.options.buildbot and self._run.options.debug:
      # If it's in debug environment, no slave builds would be triggered,
      # in order to cover the testing on pushing commits to a remote
      # temp branch, return True.
      logging.info('In debug environment, return CheckSlaveUploadPrebuiltsTest'
                   'as True')
      return True
    elif self._build_stage_id is not None and self.buildstore.AreClientsReady():
      slave_configs = self._GetSlaveConfigs()
      important_set = set([slave['name'] for slave in slave_configs])

      stages = self.buildstore.GetBuildsStages(
          buildbucket_ids=self.GetScheduledSlaveBuildbucketIds())

      passed_set = set([
          s['build_config']
          for s in stages
          if (s['name'] == stage_name and
              s['status'] == constants.BUILDER_STATUS_PASSED)
      ])

      if passed_set.issuperset(important_set):
        logging.info('All the important slaves passed %s', stage_name)
        return True
      else:
        remaining_set = important_set.difference(passed_set)
        logging.warning("slave %s didn't pass %s", remaining_set, stage_name)
        return False
    else:
      logging.warning('Not valid build_stage_id %s', self._build_stage_id)
      return False

  def PerformStage(self):
    # Either has to be a master or not have any push overlays.
    assert self._run.config.master
    assert self._run.config.push_overlays

    # If we're a commit queue, we should clean out our local changes, resync,
    # and reapply our uprevs. This is necessary so that 1) we are sure to point
    # at the remote SHA1s, not our local SHA1s; 2) we can avoid doing a
    # rebase; 3) in the case of failure and staging_branch is None, we don't
    # submit the changes that were committed locally.
    #
    # If we're not a commit queue and the build succeeded, we can skip the
    # cleanup here. This is a cheap trick so that the Chrome PFQ pushes its
    # earlier uprev from the SyncChrome stage (it would be a bit tricky to
    # replicate the uprev here, so we'll leave it alone).

    # If we're not a commit queue and staging_branch is not None, we can skip
    # the cleanup here. When staging_branch is not None, we're going to push
    # the local commits generated in AFDOUpdateEbuild stage to the
    # staging_branch, cleaning up repository here will wipe out the local
    # commits.
    if not self.success:
      repo = self.GetRepoRepository()

      # Clean up our root and sync down the latest changes that were
      # submitted.
      repo.BuildRootGitCleanup(self._build_root)

      # Sync down the latest changes we have submitted.
      if self._run.options.sync:
        next_manifest = self._run.config.manifest
        repo.Sync(next_manifest)

      # Commit uprev and portage cache regeneration locally.
      if self._run.options.uprev and self._run.config.uprev:
        commands.UprevPackages(
            self._build_root,
            self._boards,
            overlay_type=self._run.config.overlays)
        push_overlays = portage_util.FindOverlays(
            self._run.config.push_overlays, buildroot=self._build_root)
        commands.RegenPortageCache(push_overlays)

    # When prebuilts is True, if it's a successful run, update binhost conf.
    if self._run.config.prebuilts and self.success:
      confwriter = prebuilts.BinhostConfWriter(self._run)
      confwriter.Perform()

    # Push the uprev, portage cache, and binhost commits.
    commands.UprevPush(
        self._build_root,
        overlay_type=self._run.config.push_overlays,
        dryrun=self._run.options.debug)
    if config_lib.IsMasterAndroidPFQ(self._run.config) and self.success:
      self._run.attrs.metadata.UpdateWithDict({'UprevvedAndroid': True})

# -*- coding: utf-8 -*-
# Copyright (c) 2013 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 completion stages."""

from __future__ import print_function
from __future__ import division

from chromite.cbuildbot import commands
from chromite.cbuildbot import prebuilts
from chromite.cbuildbot.stages import generic_stages
from chromite.cbuildbot.stages import sync_stages
from chromite.lib import alerts
from chromite.lib import buildbucket_lib
from chromite.lib import builder_status_lib
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cros_logging as logging
from chromite.lib import failures_lib
from chromite.lib import portage_util


def GetBuilderSuccessMap(builder_run, overall_success):
  """Get the pass/fail status of all builders.

  A builder is marked as passed if all of its steps ran all of the way to
  completion. We determine this by looking at whether all of the steps for
  all of the constituent boards ran to completion.

  In cases where a builder does not have any boards, or has child boards, we
  fall back and instead just look at whether the entire build was successful.

  Args:
    builder_run: The builder run we wish to get the status of.
    overall_success: The overall status of the build.

  Returns:
    A dict, mapping the builder names to whether they succeeded.
  """
  success_map = {}
  for run in [builder_run] + builder_run.GetChildren():
    if run.config.boards and not run.config.child_configs:
      success_map[run.config.name] = True
      for board in run.config.boards:
        board_runattrs = run.GetBoardRunAttrs(board)
        if not board_runattrs.HasParallel('success'):
          success_map[run.config.name] = False
    else:
      # If a builder does not have boards, or if it has child configs, we
      # will just use the overall status instead.
      success_map[run.config.name] = overall_success
  return success_map


class ManifestVersionedSyncCompletionStage(
    generic_stages.ForgivingBuilderStage):
  """Stage that records board specific results for a unique manifest file."""

  option_name = 'sync'
  category = constants.CI_INFRA_STAGE

  def __init__(self, builder_run, buildstore, sync_stage, success, **kwargs):
    super(ManifestVersionedSyncCompletionStage, self).__init__(
        builder_run, buildstore, **kwargs)
    self.sync_stage = sync_stage
    self.success = success
    # Message that can be set that well be sent along with the status in
    # UpdateStatus.
    self.message = None

  def PerformStage(self):
    if not self.success:
      self.message = self.GetBuildFailureMessage()

    if not config_lib.IsPFQType(self._run.config.build_type):
      # Update the pass/fail status in the manifest-versions
      # repo. Suite scheduler checks the build status to schedule
      # suites.
      self._run.attrs.manifest_manager.UpdateStatus(
          success_map=GetBuilderSuccessMap(self._run, self.success),
          message=self.message)


class ImportantBuilderFailedException(failures_lib.StepFailure):
  """Exception thrown when an important build fails to build."""


class MasterSlaveSyncCompletionStage(ManifestVersionedSyncCompletionStage):
  """Stage that records whether we passed or failed to build/test manifest."""

  category = constants.CI_INFRA_STAGE

  def __init__(self, *args, **kwargs):
    super(MasterSlaveSyncCompletionStage, self).__init__(*args, **kwargs)
    # TODO(nxia): rename to _build_statuses as it contains local status and
    # slave statuses for master builds
    self._slave_statuses = {}
    self._experimental_build_statuses = {}
    self._fatal = False
    self.buildbucket_client = self.GetBuildbucketClient()

  def _WaitForSlavesToComplete(self, manager, build_identifier, builders_array,
                               timeout):
    """Wait for slave builds to complete.

    Args:
      manager: An instance of BuildSpecsManager.
      build_identifier: The BuildIdentifier instance of the master build.
      builders_array: A list of builder names (strings) of slave builds.
      timeout: Number of seconds to wait for the results.
    """
    return manager.WaitForSlavesToComplete(
        build_identifier, builders_array, timeout=timeout)

  def _GetBuilderStatusesFetcher(self):
    """Construct and return the BuilderStatusesFetcher instance.

    If this build is a master, wait for slaves to complete or timeout before
    constructing and returning the BuilderStatusesFetcher instance.

    Returns:
      A instance of builder_status_lib.BuilderStatusesFetcher.
    """
    # Wait for slaves if we're a master, in production or mock-production.
    # Otherwise just look at our own status.
    build_identifier, _ = self._run.GetCIDBHandle()
    builders_array = None
    if not self._run.config.master:
      # The slave build returns its own status.
      logging.warning('The build is not a master.')
    elif self._run.options.mock_slave_status or not self._run.options.debug:
      # The master build.
      builders = self._GetSlaveConfigs()
      builders_array = [b.name for b in builders]
      timeout = self._run.config.build_timeout

      if self._run.options.debug:
        # For debug runs, wait for three minutes to ensure most code
        # paths are executed.
        logging.info(
            'Waiting for 3 minutes only for debug run. '
            'Would have waited for %s seconds.', timeout)
        timeout = 3 * 60

      manager = self._run.attrs.manifest_manager
      if sync_stages.MasterSlaveLKGMSyncStage.external_manager:
        manager = sync_stages.MasterSlaveLKGMSyncStage.external_manager

      self._WaitForSlavesToComplete(manager, build_identifier, builders_array,
                                    timeout)

    # Set exclude_experimental to False to fetch the BuilderStatus for
    # builds which are important in config but marked as experimental in
    # the tree status.
    builder_statuses_fetcher = builder_status_lib.BuilderStatusesFetcher(
        build_identifier,
        self.buildstore,
        self.success,
        self.message,
        self._run.config,
        self._run.attrs.metadata,
        self.buildbucket_client,
        builders_array=builders_array,
        exclude_experimental=False,
        dry_run=self._run.options.debug)

    return builder_statuses_fetcher

  def _HandleStageException(self, exc_info):
    """Decide whether an exception should be treated as fatal."""
    # Besides the master, the completion stages also run on slaves, to report
    # their status back to the master. If the build failed, they throw an
    # exception here. For slave builders, marking this stage 'red' would be
    # redundant, since the build itself would already be red. In this case,
    # report a warning instead.
    # pylint: disable=protected-access
    exc_type = exc_info[0]
    if (issubclass(exc_type, ImportantBuilderFailedException) and
        not self._run.config.master):
      return self._HandleExceptionAsWarning(exc_info)
    else:
      # In all other cases, exceptions should be treated as fatal. To
      # implement this, we bypass ForgivingStage and call
      # generic_stages.BuilderStage._HandleStageException explicitly.
      return generic_stages.BuilderStage._HandleStageException(self, exc_info)

  def HandleSuccess(self):
    """Handle a successful build.

    This function is called whenever the cbuildbot run is successful.
    For the master, this will only be called when all slave builders
    are also successful. This function may be overridden by subclasses.
    """
    # We only promote for the pfq, not chrome pfq.
    # TODO(build): Run this logic in debug mode too.
    if (not self._run.options.debug and
        config_lib.IsPFQType(self._run.config.build_type) and
        self._run.config.master and self._run.manifest_branch == 'master'):
      self._run.attrs.manifest_manager.PromoteCandidate()
      if sync_stages.MasterSlaveLKGMSyncStage.external_manager:
        sync_stages.MasterSlaveLKGMSyncStage.external_manager.PromoteCandidate()

  def HandleFailure(self, failing, inflight, no_stat, self_destructed):
    """Handle a build failure.

    This function is called whenever the cbuildbot run fails.
    For the master, this will be called when any slave fails or times
    out. This function may be overridden by subclasses.

    Args:
      failing: The names of the failing builders.
      inflight: The names of the builders that are still running.
      no_stat: Set of builder names of slave builders that had status None.
      self_destructed: Boolean indicating whether the master build destructed
                       itself and stopped waiting completion of its slaves.
    """
    if failing or inflight or no_stat:
      logging.PrintBuildbotStepWarnings()

    if failing:
      logging.warning('\n'.join([
          'The following builders failed with this manifest:', ', '.join(
              sorted(failing)),
          'Please check the logs of the failing builders for details.'
      ]))

    if not self_destructed and inflight:
      logging.warning('\n'.join([
          'The following builders took too long to finish:', ', '.join(
              sorted(inflight)),
          'Please check the logs of these builders for details.'
      ]))

    if no_stat:
      logging.warning('\n'.join([
          'The following builders did not start or failed prematurely:',
          ', '.join(sorted(no_stat)),
          'Please check the logs of these builders for details.'
      ]))

  def PerformStage(self):
    super(MasterSlaveSyncCompletionStage, self).PerformStage()

    builder_statusess_fetcher = self._GetBuilderStatusesFetcher()
    self._slave_statuses, self._experimental_build_statuses = (
        builder_statusess_fetcher.GetBuilderStatuses())

    no_stat = builder_status_lib.BuilderStatusesFetcher.GetNostatBuilds(
        self._slave_statuses)
    failing = builder_status_lib.BuilderStatusesFetcher.GetFailingBuilds(
        self._slave_statuses)
    inflight = builder_status_lib.BuilderStatusesFetcher.GetInflightBuilds(
        self._slave_statuses)

    self_destructed = self._run.attrs.metadata.GetValueWithDefault(
        constants.SELF_DESTRUCTED_BUILD, False)

    self._fatal = self._IsFailureFatal(
        failing, inflight, no_stat, self_destructed=self_destructed)

    # Always annotate unsuccessful builders.
    self._AnnotateFailingBuilders(
        failing, inflight, no_stat, self._slave_statuses,
        self._experimental_build_statuses, self_destructed)

    if self._fatal:
      self.HandleFailure(failing, inflight, no_stat, self_destructed)
      raise ImportantBuilderFailedException()
    else:
      self.HandleSuccess()

  def _IsFailureFatal(self, failing, inflight, no_stat, self_destructed=False):
    """Returns a boolean indicating whether the build should fail.

    Args:
      failing: Set of build config names of builders that failed.
      inflight: Set of build config names of builders that are inflight
      no_stat: Set of build config names of builders that had status None.
      self_destructed: Boolean indicating whether it's a master build which
        destructed itself and stopped waiting its slaves to complete.

    Returns:
      True if any of the failing, inflight or no_stat builders are not sanity
      checker builders and not ignored by self-destruction; else, False.
    """
    not_passed_builders = failing | inflight | no_stat

    if self_destructed:
      # This build must be a master build if self_destructed is True.
      self_destructed_with_success = (
          self._run.attrs.metadata.GetValueWithDefault(
              constants.SELF_DESTRUCTED_WITH_SUCCESS_BUILD, False))
      if self_destructed_with_success:
        # If the master build itself didn't pass, report fatal.
        return self._run.config.name in not_passed_builders

      build_identifier, _ = self._run.GetCIDBHandle()
      if self.buildstore.AreClientsReady():
        aborted_slaves = (
            builder_status_lib.GetSlavesAbortedBySelfDestructedMaster(
                build_identifier, self.buildstore))
        # Ignore the slaves aborted by self-destruction.
        not_passed_builders -= aborted_slaves

    # Fatal if any not_passed_builders remain.
    return bool(not_passed_builders)

  def _PrintBuildMessage(self, text, url=None):
    """Print the build message.

    Args:
      text: Text (string) to print.
      url: URL (string) to link to the text, default to None.
    """
    if url is not None:
      logging.PrintBuildbotLink(text, url)
    else:
      logging.PrintBuildbotStepText(text)

  def _AnnotateNoStatBuilders(self, no_stat):
    """Annotate the build statuses fetched from the Buildbucket.

    Some builds may fail to upload statuses to GS. If the builds were
    scheduled by Buildbucket, get the build statuses and annotate the results.

    Args:
      no_stat: Config names of the slave builds with None status.
    """
    buildbucket_info_dict = buildbucket_lib.GetBuildInfoDict(
        self._run.attrs.metadata)

    for config_name in no_stat:
      if config_name in buildbucket_info_dict:
        buildbucket_id = buildbucket_info_dict[config_name].buildbucket_id
        assert buildbucket_id is not None, 'buildbucket_id is None'
        try:
          content = self.buildbucket_client.GetBuildRequest(
              buildbucket_id, self._run.options.debug)

          status = buildbucket_lib.GetBuildStatus(content)
          result = buildbucket_lib.GetBuildResult(content)

          text = '%s: [status] %s [result] %s' % (config_name, status, result)

          if result == constants.BUILDBUCKET_BUILDER_RESULT_FAILURE:
            failure_reason = buildbucket_lib.GetBuildFailureReason(content)
            if failure_reason:
              text += ' [failure_reason] %s' % failure_reason
          elif result == constants.BUILDBUCKET_BUILDER_RESULT_CANCELED:
            cancel_reason = buildbucket_lib.GetBuildCancelationReason(content)
            if cancel_reason:
              text += ' [cancelation_reason] %s' % cancel_reason

          dashboard_url = buildbucket_lib.GetBuildURL(content)
          if dashboard_url:
            logging.PrintBuildbotLink(text, dashboard_url)
          else:
            logging.PrintBuildbotStepText(text)
        except buildbucket_lib.BuildbucketResponseException as e:
          logging.error('Cannot get status for %s: %s', config_name, e)
          logging.PrintBuildbotStepText(
              'No status found for build %s buildbucket_id %s' %
              (config_name, buildbucket_id))
      else:
        logging.PrintBuildbotStepText(
            "%s wasn't scheduled by master." % config_name)

  def _AnnotateFailingBuilders(self, failing, inflight, no_stat, statuses,
                               experimental_statuses, self_destructed):
    """Annotate the failing, inflight and no_stat builds with text and links.

    Add text and buildbot links to build dashboards for failing builds and
    inflight builds. For master builds using Buildbucket schdeduler, add text
    and buildbot links for the no_stat builds; for other master builds, add
    step text for the no_stat builds.

    Args:
      failing: Set of builder names of slave builders that failed.
      inflight: Set of builder names of slave builders that are inflight.
      no_stat: Set of builder names of slave builders that had status None.
      statuses: A builder-name->status dictionary, which will provide
                the dashboard_url values for any links.
      experimental_statuses: A builder-name->status dictionary for all slaves
                             that were set as experimental through the tree
                             status.
      self_destructed: Boolean indicating whether the master build destructed
                       itself and stopped waiting completion of its slaves.
    """
    for build in failing:
      if statuses[build].message:
        self._PrintBuildMessage(
            '%s: %s' % (build, statuses[build].message.reason),
            statuses[build].dashboard_url)
      else:
        self._PrintBuildMessage('%s: failed due to unknown reasons' % build,
                                statuses[build].dashboard_url)

    if not self_destructed:
      for build in inflight:
        self._PrintBuildMessage('%s: timed out' % build,
                                statuses[build].dashboard_url)

      self._AnnotateNoStatBuilders(no_stat)
    else:
      logging.PrintBuildbotStepText('The master destructed itself and stopped '
                                    'waiting for the following slaves:')
      for build in inflight:
        self._PrintBuildMessage('%s: still running' % build,
                                statuses[build].dashboard_url)

      self._AnnotateNoStatBuilders(no_stat)

    for build, status in experimental_statuses.items():
      if not status.Passed():
        self._PrintBuildMessage(
            '%s: set as experimental through tree status' % build,
            status.dashboard_url)

  def GetSlaveStatuses(self):
    """Returns cached slave status results.

    Cached results are populated during PerformStage, so this function
    should only be called after PerformStage has returned.

    Returns:
      A dictionary from build names to builder_status_lib.BuilderStatus
      builder status objects.
    """
    return self._slave_statuses

  def GetExperimentalBuildStatuses(self):
    """Returns cached BuilderStatus of the experimental slave builds.

    Cached results are populated during PerformStage, so this function
    should only be called after PerformStage has returned.

    Returns:
      A dictionary from names of experimental slave builds to their
        builder_status_lib.BuilderStatus objects.
    """
    return self._experimental_build_statuses

  def GetFatal(self):
    """Returns whether the build (and its important slaves) failed with fatal.

    Cached results are populated during PerformStage, so this function
    should only be called after PerformStage has returned.

    Returns:
      A boolean indicating whether the build  (and its important slaves) failed
      with fatal.
    """
    return self._fatal


class CanaryCompletionStage(MasterSlaveSyncCompletionStage):
  """Collect build slave statuses and handle the failures."""

  category = constants.CI_INFRA_STAGE

  def HandleFailure(self, failing, inflight, no_stat, self_destructed):
    """Handle a build failure or timeout in the Canary builders.

    Args:
      failing: Names of the builders that failed.
      inflight: Names of the builders that timed out.
      no_stat: Set of builder names of slave builders that had status None.
      self_destructed: Boolean indicating whether the master build destructed
                       itself and stopped waiting completion of its slaves.
    """
    # Print out the status about what builds failed or not.
    MasterSlaveSyncCompletionStage.HandleFailure(self, failing, inflight,
                                                 no_stat, self_destructed)

    if self._run.config.master:
      self.CanaryMasterHandleFailure(failing, inflight, no_stat)

  def SendCanaryFailureAlert(self, failing, inflight, no_stat):
    """Send an alert email to summarize canary failures.

    Args:
      failing: The names of the failing builders.
      inflight: The names of the builders that are still running.
      no_stat: The names of the builders that had status None.
    """
    builder_name = '<NAME>'
    title = '%s has detected build failures:' % builder_name
    msgs = [
        str(x) for x in builder_status_lib.GetFailedMessages(
            self._slave_statuses, failing)
    ]
    slaves = builder_status_lib.GetBuildersWithNoneMessages(
        self._slave_statuses, failing)
    msgs += ['%s failed with unknown reason.' % x for x in slaves]
    msgs += ['%s timed out' % x for x in inflight]
    msgs += ['%s did not start' % x for x in no_stat]
    msgs.insert(0, title)
    msgs.append('You can also view the summary of the slave failures from '
                'the %s stage of %s. Click on the failure message to go '
                "to an individual slave's build status page: %s" %
                (self.name, builder_name, self.ConstructDashboardURL()))
    msg = '\n\n'.join(msgs)
    logging.warning(msg)
    extra_fields = {'X-cbuildbot-alert': 'canary-fail-alert'}
    alerts.SendHealthAlert(
        self._run, 'Canary builder failures', msg, extra_fields=extra_fields)

  def CanaryMasterHandleFailure(self, failing, inflight, no_stat):
    """Handles the failure by sending out an alert email.

    Args:
      failing: Names of the builders that failed.
      inflight: Names of the builders that timed out.
      no_stat: Set of builder names of slave builders that had status None.
    """
    if self._run.manifest_branch == 'master':
      self.SendCanaryFailureAlert(failing, inflight, no_stat)
      # Note: We used to throttle the tree here. As of
      # https://chromium-review.googlesource.com/#/c/325821/ we no longer do.

  def _HandleStageException(self, exc_info):
    """Decide whether an exception should be treated as fatal."""
    # Canary master already updates the tree status for slave
    # failures. There is no need to mark this stage red. For slave
    # builders, the build itself would already be red. In this case,
    # report a warning instead.
    # pylint: disable=protected-access
    exc_type = exc_info[0]
    if issubclass(exc_type, ImportantBuilderFailedException):
      return self._HandleExceptionAsWarning(exc_info)
    else:
      # In all other cases, exceptions should be treated as fatal.
      return super(CanaryCompletionStage, self)._HandleStageException(exc_info)


class UpdateChromeosLKGMStage(generic_stages.BuilderStage):
  """Update the CHROMEOS_LKGM file in the chromium repository."""

  category = constants.CI_INFRA_STAGE

  def PerformStage(self):
    if not self._build_threshold_successful():
      logging.info('Insufficient number of successful builders. '
                   'Skipping LKGM update.')
      return

    manager = self._run.attrs.manifest_manager
    cmd = ['chrome_chromeos_lkgm', '--lkgm=%s' % manager.current_version]
    # Always do a dryrun for now so that we can check the output and ensure it
    # is doing the correct thing.
    if self._run.options.debug:
      cmd.append('--dryrun')
    commands.RunBuildScript(self._build_root, cmd, chromite_cmd=True)

  def _build_threshold_successful(self):
    """Whether the percentage of successful child builders exceeds threshold"""
    ids = self.GetScheduledSlaveBuildbucketIds()
    num_builds = 0
    num_failures = 0
    for status in self.buildstore.GetBuildStatuses(buildbucket_ids=ids):
      if status.get('important'):
        num_builds += 1
        if status.get('status') != constants.BUILDER_STATUS_PASSED:
          num_failures += 1
    logging.info('%d of %d important builds failed.', num_failures, num_builds)
    if num_builds > 0:
      pct_succeeded = 100.0 * ((num_builds - num_failures) / float(num_builds))
      return pct_succeeded >= constants.LKGM_THRESHOLD
    return False

class PublishUprevChangesStage(generic_stages.BuilderStage):
  """Makes CQ uprev changes live for developers.

  Push local commits for uprevs, binhost, and portage cache. We resync to the
  latest version of repos as they exist in GoB.  We can't rely on the commits
  we pulled originally because our CL submit stage might have failed in some way
  (GoB sometimes flakes), or we don't want to submit all the CLs (we pushed some
  repos, but rejected others based on CQ repo settings). There might also be
  commits pushed independently (chumped by sheriffs or the precq submitted).
  """

  category = constants.CI_INFRA_STAGE

  def __init__(self,
               builder_run,
               buildstore,
               sync_stage,
               success,
               **kwargs):
    """Constructor.

    Args:
      builder_run: BuilderRun object.
      buildstore: BuildStore instance to make DB calls with.
      sync_stage: An instance of sync stage.
      success: Boolean indicating whether the build succeeded.
    """
    super(PublishUprevChangesStage, self).__init__(builder_run, buildstore,
                                                   **kwargs)
    self.sync_stage = sync_stage
    self.success = success

  def CheckMasterBinhostTest(self, buildbucket_id):
    """Check whether the master builder has passed BinhostTest stage.

    Args:
      buildbucket_id: buildbucket_id of the master build to check for.

    Returns:
      True if the status of the master build BinhostTest stage is 'pass';
      else, False.
    """
    stage_name = 'BinhostTest'

    if self._build_stage_id is not None and self.buildstore.AreClientsReady():
      stages = self.buildstore.GetBuildsStages(buildbucket_ids=[buildbucket_id])

      # No stages found. BinhostTest stage didn't start or got skipped,
      # in both case we don't need to push commits to the temp pfq branch.
      if not stages:
        logging.warning('no %s stage found in build %s',
                        stage_name, buildbucket_id)
        return False

      stage_status = [
          s for s in stages if (s['name'] == stage_name and
                                s['status'] == constants.BUILDER_STATUS_PASSED)
      ]
      if stage_status:
        logging.info('build %s passed stage %s with %s', buildbucket_id,
                     stage_name, stage_status)
        return True
      else:
        logging.warning('build %s stage %s result %s', buildbucket_id,
                        stage_name, stage_status)
        return False

    logging.warning('Not valid build_stage_id %s or no %s found',
                    self._build_stage_id, stage_name)
    return False

  def CheckSlaveUploadPrebuiltsTest(self):
    """Check if the slaves have passed UploadPrebuilts stage.

    Given the master build id, check if all the important slaves have passed
    the UploadPrebuilts stage.

    Returns:
      True if all the important slaves have passed the stage;
      True if it's in debug environment;
      else, False.
    """
    stage_name = 'UploadPrebuilts'

    if not self._run.config.master:
      logging.warning('The build is not a master')
      return False
    elif self._run.options.buildbot and self._run.options.debug:
      # If it's in debug environment, no slave builds would be triggered,
      # in order to cover the testing on pushing commits to a remote
      # temp branch, return True.
      logging.info('In debug environment, return CheckSlaveUploadPrebuiltsTest'
                   'as True')
      return True
    elif self._build_stage_id is not None and self.buildstore.AreClientsReady():
      slave_configs = self._GetSlaveConfigs()
      important_set = set([slave['name'] for slave in slave_configs])

      stages = self.buildstore.GetBuildsStages(
          buildbucket_ids=self.GetScheduledSlaveBuildbucketIds())

      passed_set = set([
          s['build_config']
          for s in stages
          if (s['name'] == stage_name and
              s['status'] == constants.BUILDER_STATUS_PASSED)
      ])

      if passed_set.issuperset(important_set):
        logging.info('All the important slaves passed %s', stage_name)
        return True
      else:
        remaining_set = important_set.difference(passed_set)
        logging.warning("slave %s didn't pass %s", remaining_set, stage_name)
        return False
    else:
      logging.warning('Not valid build_stage_id %s', self._build_stage_id)
      return False

  def PerformStage(self):
    # Either has to be a master or not have any push overlays.
    assert self._run.config.master
    assert self._run.config.push_overlays

    # If we're a commit queue, we should clean out our local changes, resync,
    # and reapply our uprevs. This is necessary so that 1) we are sure to point
    # at the remote SHA1s, not our local SHA1s; 2) we can avoid doing a
    # rebase; 3) in the case of failure and staging_branch is None, we don't
    # submit the changes that were committed locally.
    #
    # If we're not a commit queue and the build succeeded, we can skip the
    # cleanup here. This is a cheap trick so that the Chrome PFQ pushes its
    # earlier uprev from the SyncChrome stage (it would be a bit tricky to
    # replicate the uprev here, so we'll leave it alone).

    # If we're not a commit queue and staging_branch is not None, we can skip
    # the cleanup here. When staging_branch is not None, we're going to push
    # the local commits generated in AFDOUpdateEbuild stage to the
    # staging_branch, cleaning up repository here will wipe out the local
    # commits.
    if not self.success:
      repo = self.GetRepoRepository()

      # Clean up our root and sync down the latest changes that were
      # submitted.
      repo.BuildRootGitCleanup(self._build_root)

      # Sync down the latest changes we have submitted.
      if self._run.options.sync:
        next_manifest = self._run.config.manifest
        repo.Sync(next_manifest)

      # Commit uprev and portage cache regeneration locally.
      if self._run.options.uprev and self._run.config.uprev:
        commands.UprevPackages(
            self._build_root,
            self._boards,
            overlay_type=self._run.config.overlays)
        push_overlays = portage_util.FindOverlays(
            self._run.config.push_overlays, buildroot=self._build_root)
        commands.RegenPortageCache(push_overlays)

    # When prebuilts is True, if it's a successful run, update binhost conf.
    if self._run.config.prebuilts and self.success:
      confwriter = prebuilts.BinhostConfWriter(self._run)
      confwriter.Perform()

    # Push the uprev, portage cache, and binhost commits.
    commands.UprevPush(
        self._build_root,
        overlay_type=self._run.config.push_overlays,
        dryrun=self._run.options.debug)
    if config_lib.IsMasterAndroidPFQ(self._run.config) and self.success:
      self._run.attrs.metadata.UpdateWithDict({'UprevvedAndroid': True})
# -*- coding: utf-8 -*- # Copyright (c) 2013 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 completion stages. Get the pass/fail status of all builders.    A builder is marked as passed if all of its steps ran all of the way to   completion. We determine this by looking at whether all of the steps for   all of the constituent boards ran to completion.    In cases where a builder does not have any boards, or has child boards, we   fall back and instead just look at whether the entire build was successful.    Args:     builder_run: The builder run we wish to get the status of.     overall_success: The overall status of the build.    Returns:     A dict, mapping the builder names to whether they succeeded. # If a builder does not have boards, or if it has child configs, we # will just use the overall status instead. Stage that records board specific results for a unique manifest file. # Message that can be set that well be sent along with the status in # UpdateStatus. # Update the pass/fail status in the manifest-versions # repo. Suite scheduler checks the build status to schedule # suites. Exception thrown when an important build fails to build. Stage that records whether we passed or failed to build/test manifest. # TODO(nxia): rename to _build_statuses as it contains local status and # slave statuses for master builds Wait for slave builds to complete.      Args:       manager: An instance of BuildSpecsManager.       build_identifier: The BuildIdentifier instance of the master build.       builders_array: A list of builder names (strings) of slave builds.       timeout: Number of seconds to wait for the results. Construct and return the BuilderStatusesFetcher instance.      If this build is a master, wait for slaves to complete or timeout before     constructing and returning the BuilderStatusesFetcher instance.      Returns:       A instance of builder_status_lib.BuilderStatusesFetcher. # Wait for slaves if we're a master, in production or mock-production. # Otherwise just look at our own status. # The slave build returns its own status. # The master build. # For debug runs, wait for three minutes to ensure most code # paths are executed. # Set exclude_experimental to False to fetch the BuilderStatus for # builds which are important in config but marked as experimental in # the tree status. Decide whether an exception should be treated as fatal. # Besides the master, the completion stages also run on slaves, to report # their status back to the master. If the build failed, they throw an # exception here. For slave builders, marking this stage 'red' would be # redundant, since the build itself would already be red. In this case, # report a warning instead. # pylint: disable=protected-access # In all other cases, exceptions should be treated as fatal. To # implement this, we bypass ForgivingStage and call # generic_stages.BuilderStage._HandleStageException explicitly. Handle a successful build.      This function is called whenever the cbuildbot run is successful.     For the master, this will only be called when all slave builders     are also successful. This function may be overridden by subclasses. # We only promote for the pfq, not chrome pfq. # TODO(build): Run this logic in debug mode too. Handle a build failure.      This function is called whenever the cbuildbot run fails.     For the master, this will be called when any slave fails or times     out. This function may be overridden by subclasses.      Args:       failing: The names of the failing builders.       inflight: The names of the builders that are still running.       no_stat: Set of builder names of slave builders that had status None.       self_destructed: Boolean indicating whether the master build destructed                        itself and stopped waiting completion of its slaves. # Always annotate unsuccessful builders. Returns a boolean indicating whether the build should fail.      Args:       failing: Set of build config names of builders that failed.       inflight: Set of build config names of builders that are inflight       no_stat: Set of build config names of builders that had status None.       self_destructed: Boolean indicating whether it's a master build which         destructed itself and stopped waiting its slaves to complete.      Returns:       True if any of the failing, inflight or no_stat builders are not sanity       checker builders and not ignored by self-destruction; else, False. # This build must be a master build if self_destructed is True. # If the master build itself didn't pass, report fatal. # Ignore the slaves aborted by self-destruction. # Fatal if any not_passed_builders remain. Print the build message.      Args:       text: Text (string) to print.       url: URL (string) to link to the text, default to None. Annotate the build statuses fetched from the Buildbucket.      Some builds may fail to upload statuses to GS. If the builds were     scheduled by Buildbucket, get the build statuses and annotate the results.      Args:       no_stat: Config names of the slave builds with None status. Annotate the failing, inflight and no_stat builds with text and links.      Add text and buildbot links to build dashboards for failing builds and     inflight builds. For master builds using Buildbucket schdeduler, add text     and buildbot links for the no_stat builds; for other master builds, add     step text for the no_stat builds.      Args:       failing: Set of builder names of slave builders that failed.       inflight: Set of builder names of slave builders that are inflight.       no_stat: Set of builder names of slave builders that had status None.       statuses: A builder-name->status dictionary, which will provide                 the dashboard_url values for any links.       experimental_statuses: A builder-name->status dictionary for all slaves                              that were set as experimental through the tree                              status.       self_destructed: Boolean indicating whether the master build destructed                        itself and stopped waiting completion of its slaves. Returns cached slave status results.      Cached results are populated during PerformStage, so this function     should only be called after PerformStage has returned.      Returns:       A dictionary from build names to builder_status_lib.BuilderStatus       builder status objects. Returns cached BuilderStatus of the experimental slave builds.      Cached results are populated during PerformStage, so this function     should only be called after PerformStage has returned.      Returns:       A dictionary from names of experimental slave builds to their         builder_status_lib.BuilderStatus objects. Returns whether the build (and its important slaves) failed with fatal.      Cached results are populated during PerformStage, so this function     should only be called after PerformStage has returned.      Returns:       A boolean indicating whether the build  (and its important slaves) failed       with fatal. Collect build slave statuses and handle the failures. Handle a build failure or timeout in the Canary builders.      Args:       failing: Names of the builders that failed.       inflight: Names of the builders that timed out.       no_stat: Set of builder names of slave builders that had status None.       self_destructed: Boolean indicating whether the master build destructed                        itself and stopped waiting completion of its slaves. # Print out the status about what builds failed or not. Send an alert email to summarize canary failures.      Args:       failing: The names of the failing builders.       inflight: The names of the builders that are still running.       no_stat: The names of the builders that had status None. Handles the failure by sending out an alert email.      Args:       failing: Names of the builders that failed.       inflight: Names of the builders that timed out.       no_stat: Set of builder names of slave builders that had status None. # Note: We used to throttle the tree here. As of # https://chromium-review.googlesource.com/#/c/325821/ we no longer do. Decide whether an exception should be treated as fatal. # Canary master already updates the tree status for slave # failures. There is no need to mark this stage red. For slave # builders, the build itself would already be red. In this case, # report a warning instead. # pylint: disable=protected-access # In all other cases, exceptions should be treated as fatal. Update the CHROMEOS_LKGM file in the chromium repository. # Always do a dryrun for now so that we can check the output and ensure it # is doing the correct thing. Whether the percentage of successful child builders exceeds threshold Makes CQ uprev changes live for developers.    Push local commits for uprevs, binhost, and portage cache. We resync to the   latest version of repos as they exist in GoB.  We can't rely on the commits   we pulled originally because our CL submit stage might have failed in some way   (GoB sometimes flakes), or we don't want to submit all the CLs (we pushed some   repos, but rejected others based on CQ repo settings). There might also be   commits pushed independently (chumped by sheriffs or the precq submitted). Constructor.      Args:       builder_run: BuilderRun object.       buildstore: BuildStore instance to make DB calls with.       sync_stage: An instance of sync stage.       success: Boolean indicating whether the build succeeded. Check whether the master builder has passed BinhostTest stage.      Args:       buildbucket_id: buildbucket_id of the master build to check for.      Returns:       True if the status of the master build BinhostTest stage is 'pass';       else, False. # No stages found. BinhostTest stage didn't start or got skipped, # in both case we don't need to push commits to the temp pfq branch. Check if the slaves have passed UploadPrebuilts stage.      Given the master build id, check if all the important slaves have passed     the UploadPrebuilts stage.      Returns:       True if all the important slaves have passed the stage;       True if it's in debug environment;       else, False. # If it's in debug environment, no slave builds would be triggered, # in order to cover the testing on pushing commits to a remote # temp branch, return True. # Either has to be a master or not have any push overlays. # If we're a commit queue, we should clean out our local changes, resync, # and reapply our uprevs. This is necessary so that 1) we are sure to point # at the remote SHA1s, not our local SHA1s; 2) we can avoid doing a # rebase; 3) in the case of failure and staging_branch is None, we don't # submit the changes that were committed locally. # # If we're not a commit queue and the build succeeded, we can skip the # cleanup here. This is a cheap trick so that the Chrome PFQ pushes its # earlier uprev from the SyncChrome stage (it would be a bit tricky to # replicate the uprev here, so we'll leave it alone). # If we're not a commit queue and staging_branch is not None, we can skip # the cleanup here. When staging_branch is not None, we're going to push # the local commits generated in AFDOUpdateEbuild stage to the # staging_branch, cleaning up repository here will wipe out the local # commits. # Clean up our root and sync down the latest changes that were # submitted. # Sync down the latest changes we have submitted. # Commit uprev and portage cache regeneration locally. # When prebuilts is True, if it's a successful run, update binhost conf. # Push the uprev, portage cache, and binhost commits.