普通文本  |  411行  |  11.75 KB

#!/usr/bin/env python2
"""Generate summary report for ChromeOS toolchain waterfalls."""

from __future__ import print_function

import argparse
import datetime
import getpass
import json
import os
import re
import shutil
import sys
import time

from cros_utils import command_executer

# All the test suites whose data we might want for the reports.
TESTS = (('bvt-inline', 'HWTest [bvt-inline]'), ('bvt-cq', 'HWTest [bvt-cq]'),
         ('security', 'HWTest [security]'))

# The main waterfall builders, IN THE ORDER IN WHICH WE WANT THEM
# LISTED IN THE REPORT.
WATERFALL_BUILDERS = [
    'amd64-llvm-next-toolchain',
    'arm-llvm-next-toolchain',
    'arm64-llvm-next-toolchain',
]

DATA_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-report-data/'
ARCHIVE_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-reports/'
DOWNLOAD_DIR = '/tmp/waterfall-logs'
MAX_SAVE_RECORDS = 7
BUILD_DATA_FILE = '%s/build-data.txt' % DATA_DIR
LLVM_ROTATING_BUILDER = 'llvm_next_toolchain'
ROTATING_BUILDERS = [LLVM_ROTATING_BUILDER]

# For int-to-string date conversion.  Note, the index of the month in this
# list needs to correspond to the month's integer value.  i.e. 'Sep' must
# be as MONTHS[9].
MONTHS = [
    '', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
    'Nov', 'Dec'
]

DAYS_PER_MONTH = {
    1: 31,
    2: 28,
    3: 31,
    4: 30,
    5: 31,
    6: 30,
    7: 31,
    8: 31,
    9: 30,
    10: 31,
    11: 31,
    12: 31
}


def format_date(int_date, use_int_month=False):
  """Convert an integer date to a string date. YYYYMMDD -> YYYY-MMM-DD"""

  if int_date == 0:
    return 'today'

  tmp_date = int_date
  day = tmp_date % 100
  tmp_date = tmp_date / 100
  month = tmp_date % 100
  year = tmp_date / 100

  if use_int_month:
    date_str = '%d-%02d-%02d' % (year, month, day)
  else:
    month_str = MONTHS[month]
    date_str = '%d-%s-%d' % (year, month_str, day)
  return date_str


def EmailReport(report_file, report_type, date, email_to):
  """Emails the report to the approprite address."""
  subject = '%s Waterfall Summary report, %s' % (report_type, date)
  sendgmr_path = '/google/data/ro/projects/gws-sre/sendgmr'
  command = ('%s --to=%s --subject="%s" --body_file=%s' %
             (sendgmr_path, email_to, subject, report_file))
  command_executer.GetCommandExecuter().RunCommand(command)


def GetColor(status):
  """Given a job status string, returns appropriate color string."""
  if status.strip() == 'pass':
    color = 'green '
  elif status.strip() == 'fail':
    color = ' red  '
  elif status.strip() == 'warning':
    color = 'orange'
  else:
    color = '      '
  return color


def GenerateWaterfallReport(report_dict, waterfall_type, date):
  """Write out the actual formatted report."""

  filename = 'waterfall_report.%s_waterfall.%s.txt' % (waterfall_type, date)

  date_string = ''
  report_list = report_dict.keys()

  with open(filename, 'w') as out_file:
    # Write Report Header
    out_file.write('\nStatus of %s Waterfall Builds from %s\n\n' %
                   (waterfall_type, date_string))
    out_file.write('                                                        \n')
    out_file.write(
        '                                         Build       bvt-    '
        '     bvt-cq     '
        ' security \n')
    out_file.write(
        '                                         status     inline   '
        '              \n')

    # Write daily waterfall status section.
    for builder in report_list:
      build_dict = report_dict[builder]
      buildbucket_id = build_dict['buildbucket_id']
      overall_status = build_dict['status']
      if 'bvt-inline' in build_dict.keys():
        inline_status = build_dict['bvt-inline']
      else:
        inline_status = '    '
      if 'bvt-cq' in build_dict.keys():
        cq_status = build_dict['bvt-cq']
      else:
        cq_status = '    '
      if 'security' in build_dict.keys():
        security_status = build_dict['security']
      else:
        security_status = '    '
      inline_color = GetColor(inline_status)
      cq_color = GetColor(cq_status)
      security_color = GetColor(security_status)

      out_file.write(
          '%26s   %4s        %6s        %6s         %6s\n' %
          (builder, overall_status, inline_color, cq_color, security_color))
      if waterfall_type == 'main':
        out_file.write('     build url: https://cros-goldeneye.corp.google.com/'
                       'chromeos/healthmonitoring/buildDetails?buildbucketId=%s'
                       '\n' % buildbucket_id)
      else:
        out_file.write('     build url: https://ci.chromium.org/p/chromeos/'
                       'builds/b%s \n' % buildbucket_id)
        report_url = ('https://logs.chromium.org/v/?s=chromeos%2Fbuildbucket%2F'
                      'cr-buildbucket.appspot.com%2F' + buildbucket_id +
                      '%2F%2B%2Fsteps%2FReport%2F0%2Fstdout')
        out_file.write('\n     report status url: %s\n' % report_url)
      out_file.write('\n')

    print('Report generated in %s.' % filename)
    return filename


def GetTryjobData(date, rotating_builds_dict):
  """Read buildbucket id and board from stored file.

  buildbot_test_llvm.py, when it launches the rotating builders,
  records the buildbucket_id and board for each launch in a file.
  This reads that data out of the file so we can find the right
  tryjob data.
  """

  date_str = format_date(date, use_int_month=True)
  fname = '%s.builds' % date_str
  filename = os.path.join(DATA_DIR, 'rotating-builders', fname)

  if not os.path.exists(filename):
    print('Cannot find file: %s' % filename)
    print('Unable to generate rotating builder report for date %d.' % date)
    return

  with open(filename, 'r') as in_file:
    lines = in_file.readlines()

  for line in lines:
    l = line.strip()
    parts = l.split(',')
    if len(parts) != 2:
      print('Warning: Illegal line in data file.')
      print('File: %s' % filename)
      print('Line: %s' % l)
      continue
    buildbucket_id = parts[0]
    board = parts[1]
    rotating_builds_dict[board] = buildbucket_id

  return


def GetRotatingBuildData(date, report_dict, chromeos_root, board,
                         buildbucket_id, ce):
  """Gets rotating builder job results via 'cros buildresult'."""
  path = os.path.join(chromeos_root, 'chromite')
  save_dir = os.getcwd()
  date_str = format_date(date, use_int_month=True)
  os.chdir(path)

  command = (
      'cros buildresult --buildbucket-id %s --report json' % buildbucket_id)
  _, out, _ = ce.RunCommandWOutput(command)
  tmp_dict = json.loads(out)
  results = tmp_dict[buildbucket_id]

  board_dict = dict()
  board_dict['buildbucket_id'] = buildbucket_id
  stages_results = results['stages']
  for test in TESTS:
    key1 = test[0]
    key2 = test[1]
    if key2 in stages_results:
      board_dict[key1] = stages_results[key2]
  board_dict['status'] = results['status']
  report_dict[board] = board_dict
  os.chdir(save_dir)
  return


def GetMainWaterfallData(date, report_dict, chromeos_root, ce):
  """Gets main waterfall job results via 'cros buildresult'."""
  path = os.path.join(chromeos_root, 'chromite')
  save_dir = os.getcwd()
  date_str = format_date(date, use_int_month=True)
  os.chdir(path)
  for builder in WATERFALL_BUILDERS:
    command = ('cros buildresult --build-config %s --date %s --report json' %
               (builder, date_str))
    _, out, _ = ce.RunCommandWOutput(command)
    tmp_dict = json.loads(out)
    builder_dict = dict()
    for k in tmp_dict.keys():
      buildbucket_id = k
      results = tmp_dict[k]

    builder_dict['buildbucket_id'] = buildbucket_id
    builder_dict['status'] = results['status']
    stages_results = results['stages']
    for test in TESTS:
      key1 = test[0]
      key2 = test[1]
      builder_dict[key1] = stages_results[key2]
    report_dict[builder] = builder_dict
  os.chdir(save_dir)
  return


# Check for prodaccess.
def CheckProdAccess():
  """Verifies prodaccess is current."""
  status, output, _ = command_executer.GetCommandExecuter().RunCommandWOutput(
      'prodcertstatus')
  if status != 0:
    return False
  # Verify that status is not expired
  if 'expires' in output:
    return True
  return False


def ValidDate(date):
  """Ensures 'date' is a valid date."""
  min_year = 2018

  tmp_date = date
  day = tmp_date % 100
  tmp_date = tmp_date / 100
  month = tmp_date % 100
  year = tmp_date / 100

  if day < 1 or month < 1 or year < min_year:
    return False

  cur_year = datetime.datetime.now().year
  if year > cur_year:
    return False

  if month > 12:
    return False

  if month == 2 and cur_year % 4 == 0 and cur_year % 100 != 0:
    max_day = 29
  else:
    max_day = DAYS_PER_MONTH[month]

  if day > max_day:
    return False

  return True


def ValidOptions(parser, options):
  """Error-check the options passed to this script."""
  too_many_options = False
  if options.main:
    if options.rotating:
      too_many_options = True

  if too_many_options:
    parser.error('Can only specify one of --main, --rotating.')

  if not os.path.exists(options.chromeos_root):
    parser.error(
        'Invalid chromeos root. Cannot find: %s' % options.chromeos_root)

  email_ok = True
  if options.email and options.email.find('@') == -1:
    email_ok = False
    parser.error('"%s" is not a valid email address; it must contain "@..."' %
                 options.email)

  valid_date = ValidDate(options.date)

  return not too_many_options and valid_date and email_ok


def Main(argv):
  """Main function for this script."""
  parser = argparse.ArgumentParser()
  parser.add_argument(
      '--main',
      dest='main',
      default=False,
      action='store_true',
      help='Generate report only for main waterfall '
      'builders.')
  parser.add_argument(
      '--rotating',
      dest='rotating',
      default=False,
      action='store_true',
      help='Generate report only for rotating builders.')
  parser.add_argument(
      '--date',
      dest='date',
      required=True,
      type=int,
      help='The date YYYYMMDD of waterfall report.')
  parser.add_argument(
      '--email',
      dest='email',
      default='',
      help='Email address to use for sending the report.')
  parser.add_argument(
      '--chromeos_root',
      dest='chromeos_root',
      required=True,
      help='Chrome OS root in which to run chroot commands.')

  options = parser.parse_args(argv)

  if not ValidOptions(parser, options):
    return 1

  main_only = options.main
  rotating_only = options.rotating
  date = options.date

  prod_access = CheckProdAccess()
  if not prod_access:
    print('ERROR: Please run prodaccess first.')
    return

  waterfall_report_dict = dict()
  rotating_report_dict = dict()

  ce = command_executer.GetCommandExecuter()
  if not rotating_only:
    GetMainWaterfallData(date, waterfall_report_dict, options.chromeos_root, ce)

  if not main_only:
    rotating_builds_dict = dict()
    GetTryjobData(date, rotating_builds_dict)
    if len(rotating_builds_dict.keys()) > 0:
      for board in rotating_builds_dict.keys():
        buildbucket_id = rotating_builds_dict[board]
        GetRotatingBuildData(date, rotating_report_dict, options.chromeos_root,
                             board, buildbucket_id, ce)

  if options.email:
    email_to = options.email
  else:
    email_to = getpass.getuser()

  if waterfall_report_dict and not rotating_only:
    main_report = GenerateWaterfallReport(waterfall_report_dict, 'main', date)

    EmailReport(main_report, 'Main', format_date(date), email_to)
    shutil.copy(main_report, ARCHIVE_DIR)
  if rotating_report_dict and not main_only:
    rotating_report = GenerateWaterfallReport(rotating_report_dict, 'rotating',
                                              date)

    EmailReport(rotating_report, 'Rotating', format_date(date), email_to)
    shutil.copy(rotating_report, ARCHIVE_DIR)


if __name__ == '__main__':
  Main(sys.argv[1:])
  sys.exit(0)