1#!/usr/bin/env python2
2"""Generate summary report for ChromeOS toolchain waterfalls."""
3
4from __future__ import print_function
5
6import argparse
7import datetime
8import getpass
9import json
10import os
11import re
12import shutil
13import sys
14import time
15
16from cros_utils import command_executer
17
18# All the test suites whose data we might want for the reports.
19TESTS = (('bvt-inline', 'HWTest [bvt-inline]'), ('bvt-cq', 'HWTest [bvt-cq]'),
20         ('security', 'HWTest [security]'))
21
22# The main waterfall builders, IN THE ORDER IN WHICH WE WANT THEM
23# LISTED IN THE REPORT.
24WATERFALL_BUILDERS = [
25    'amd64-llvm-next-toolchain',
26    'arm-llvm-next-toolchain',
27    'arm64-llvm-next-toolchain',
28]
29
30DATA_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-report-data/'
31ARCHIVE_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-reports/'
32DOWNLOAD_DIR = '/tmp/waterfall-logs'
33MAX_SAVE_RECORDS = 7
34BUILD_DATA_FILE = '%s/build-data.txt' % DATA_DIR
35LLVM_ROTATING_BUILDER = 'llvm_next_toolchain'
36ROTATING_BUILDERS = [LLVM_ROTATING_BUILDER]
37
38# For int-to-string date conversion.  Note, the index of the month in this
39# list needs to correspond to the month's integer value.  i.e. 'Sep' must
40# be as MONTHS[9].
41MONTHS = [
42    '', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
43    'Nov', 'Dec'
44]
45
46DAYS_PER_MONTH = {
47    1: 31,
48    2: 28,
49    3: 31,
50    4: 30,
51    5: 31,
52    6: 30,
53    7: 31,
54    8: 31,
55    9: 30,
56    10: 31,
57    11: 31,
58    12: 31
59}
60
61
62def format_date(int_date, use_int_month=False):
63  """Convert an integer date to a string date. YYYYMMDD -> YYYY-MMM-DD"""
64
65  if int_date == 0:
66    return 'today'
67
68  tmp_date = int_date
69  day = tmp_date % 100
70  tmp_date = tmp_date / 100
71  month = tmp_date % 100
72  year = tmp_date / 100
73
74  if use_int_month:
75    date_str = '%d-%02d-%02d' % (year, month, day)
76  else:
77    month_str = MONTHS[month]
78    date_str = '%d-%s-%d' % (year, month_str, day)
79  return date_str
80
81
82def EmailReport(report_file, report_type, date, email_to):
83  """Emails the report to the approprite address."""
84  subject = '%s Waterfall Summary report, %s' % (report_type, date)
85  sendgmr_path = '/google/data/ro/projects/gws-sre/sendgmr'
86  command = ('%s --to=%s --subject="%s" --body_file=%s' %
87             (sendgmr_path, email_to, subject, report_file))
88  command_executer.GetCommandExecuter().RunCommand(command)
89
90
91def GetColor(status):
92  """Given a job status string, returns appropriate color string."""
93  if status.strip() == 'pass':
94    color = 'green '
95  elif status.strip() == 'fail':
96    color = ' red  '
97  elif status.strip() == 'warning':
98    color = 'orange'
99  else:
100    color = '      '
101  return color
102
103
104def GenerateWaterfallReport(report_dict, waterfall_type, date):
105  """Write out the actual formatted report."""
106
107  filename = 'waterfall_report.%s_waterfall.%s.txt' % (waterfall_type, date)
108
109  date_string = ''
110  report_list = report_dict.keys()
111
112  with open(filename, 'w') as out_file:
113    # Write Report Header
114    out_file.write('\nStatus of %s Waterfall Builds from %s\n\n' %
115                   (waterfall_type, date_string))
116    out_file.write('                                                        \n')
117    out_file.write(
118        '                                         Build       bvt-    '
119        '     bvt-cq     '
120        ' security \n')
121    out_file.write(
122        '                                         status     inline   '
123        '              \n')
124
125    # Write daily waterfall status section.
126    for builder in report_list:
127      build_dict = report_dict[builder]
128      buildbucket_id = build_dict['buildbucket_id']
129      overall_status = build_dict['status']
130      if 'bvt-inline' in build_dict.keys():
131        inline_status = build_dict['bvt-inline']
132      else:
133        inline_status = '    '
134      if 'bvt-cq' in build_dict.keys():
135        cq_status = build_dict['bvt-cq']
136      else:
137        cq_status = '    '
138      if 'security' in build_dict.keys():
139        security_status = build_dict['security']
140      else:
141        security_status = '    '
142      inline_color = GetColor(inline_status)
143      cq_color = GetColor(cq_status)
144      security_color = GetColor(security_status)
145
146      out_file.write(
147          '%26s   %4s        %6s        %6s         %6s\n' %
148          (builder, overall_status, inline_color, cq_color, security_color))
149      if waterfall_type == 'main':
150        out_file.write('     build url: https://cros-goldeneye.corp.google.com/'
151                       'chromeos/healthmonitoring/buildDetails?buildbucketId=%s'
152                       '\n' % buildbucket_id)
153      else:
154        out_file.write('     build url: https://ci.chromium.org/p/chromeos/'
155                       'builds/b%s \n' % buildbucket_id)
156        report_url = ('https://logs.chromium.org/v/?s=chromeos%2Fbuildbucket%2F'
157                      'cr-buildbucket.appspot.com%2F' + buildbucket_id +
158                      '%2F%2B%2Fsteps%2FReport%2F0%2Fstdout')
159        out_file.write('\n     report status url: %s\n' % report_url)
160      out_file.write('\n')
161
162    print('Report generated in %s.' % filename)
163    return filename
164
165
166def GetTryjobData(date, rotating_builds_dict):
167  """Read buildbucket id and board from stored file.
168
169  buildbot_test_llvm.py, when it launches the rotating builders,
170  records the buildbucket_id and board for each launch in a file.
171  This reads that data out of the file so we can find the right
172  tryjob data.
173  """
174
175  date_str = format_date(date, use_int_month=True)
176  fname = '%s.builds' % date_str
177  filename = os.path.join(DATA_DIR, 'rotating-builders', fname)
178
179  if not os.path.exists(filename):
180    print('Cannot find file: %s' % filename)
181    print('Unable to generate rotating builder report for date %d.' % date)
182    return
183
184  with open(filename, 'r') as in_file:
185    lines = in_file.readlines()
186
187  for line in lines:
188    l = line.strip()
189    parts = l.split(',')
190    if len(parts) != 2:
191      print('Warning: Illegal line in data file.')
192      print('File: %s' % filename)
193      print('Line: %s' % l)
194      continue
195    buildbucket_id = parts[0]
196    board = parts[1]
197    rotating_builds_dict[board] = buildbucket_id
198
199  return
200
201
202def GetRotatingBuildData(date, report_dict, chromeos_root, board,
203                         buildbucket_id, ce):
204  """Gets rotating builder job results via 'cros buildresult'."""
205  path = os.path.join(chromeos_root, 'chromite')
206  save_dir = os.getcwd()
207  date_str = format_date(date, use_int_month=True)
208  os.chdir(path)
209
210  command = (
211      'cros buildresult --buildbucket-id %s --report json' % buildbucket_id)
212  _, out, _ = ce.RunCommandWOutput(command)
213  tmp_dict = json.loads(out)
214  results = tmp_dict[buildbucket_id]
215
216  board_dict = dict()
217  board_dict['buildbucket_id'] = buildbucket_id
218  stages_results = results['stages']
219  for test in TESTS:
220    key1 = test[0]
221    key2 = test[1]
222    if key2 in stages_results:
223      board_dict[key1] = stages_results[key2]
224  board_dict['status'] = results['status']
225  report_dict[board] = board_dict
226  os.chdir(save_dir)
227  return
228
229
230def GetMainWaterfallData(date, report_dict, chromeos_root, ce):
231  """Gets main waterfall job results via 'cros buildresult'."""
232  path = os.path.join(chromeos_root, 'chromite')
233  save_dir = os.getcwd()
234  date_str = format_date(date, use_int_month=True)
235  os.chdir(path)
236  for builder in WATERFALL_BUILDERS:
237    command = ('cros buildresult --build-config %s --date %s --report json' %
238               (builder, date_str))
239    _, out, _ = ce.RunCommandWOutput(command)
240    tmp_dict = json.loads(out)
241    builder_dict = dict()
242    for k in tmp_dict.keys():
243      buildbucket_id = k
244      results = tmp_dict[k]
245
246    builder_dict['buildbucket_id'] = buildbucket_id
247    builder_dict['status'] = results['status']
248    stages_results = results['stages']
249    for test in TESTS:
250      key1 = test[0]
251      key2 = test[1]
252      builder_dict[key1] = stages_results[key2]
253    report_dict[builder] = builder_dict
254  os.chdir(save_dir)
255  return
256
257
258# Check for prodaccess.
259def CheckProdAccess():
260  """Verifies prodaccess is current."""
261  status, output, _ = command_executer.GetCommandExecuter().RunCommandWOutput(
262      'prodcertstatus')
263  if status != 0:
264    return False
265  # Verify that status is not expired
266  if 'expires' in output:
267    return True
268  return False
269
270
271def ValidDate(date):
272  """Ensures 'date' is a valid date."""
273  min_year = 2018
274
275  tmp_date = date
276  day = tmp_date % 100
277  tmp_date = tmp_date / 100
278  month = tmp_date % 100
279  year = tmp_date / 100
280
281  if day < 1 or month < 1 or year < min_year:
282    return False
283
284  cur_year = datetime.datetime.now().year
285  if year > cur_year:
286    return False
287
288  if month > 12:
289    return False
290
291  if month == 2 and cur_year % 4 == 0 and cur_year % 100 != 0:
292    max_day = 29
293  else:
294    max_day = DAYS_PER_MONTH[month]
295
296  if day > max_day:
297    return False
298
299  return True
300
301
302def ValidOptions(parser, options):
303  """Error-check the options passed to this script."""
304  too_many_options = False
305  if options.main:
306    if options.rotating:
307      too_many_options = True
308
309  if too_many_options:
310    parser.error('Can only specify one of --main, --rotating.')
311
312  if not os.path.exists(options.chromeos_root):
313    parser.error(
314        'Invalid chromeos root. Cannot find: %s' % options.chromeos_root)
315
316  email_ok = True
317  if options.email and options.email.find('@') == -1:
318    email_ok = False
319    parser.error('"%s" is not a valid email address; it must contain "@..."' %
320                 options.email)
321
322  valid_date = ValidDate(options.date)
323
324  return not too_many_options and valid_date and email_ok
325
326
327def Main(argv):
328  """Main function for this script."""
329  parser = argparse.ArgumentParser()
330  parser.add_argument(
331      '--main',
332      dest='main',
333      default=False,
334      action='store_true',
335      help='Generate report only for main waterfall '
336      'builders.')
337  parser.add_argument(
338      '--rotating',
339      dest='rotating',
340      default=False,
341      action='store_true',
342      help='Generate report only for rotating builders.')
343  parser.add_argument(
344      '--date',
345      dest='date',
346      required=True,
347      type=int,
348      help='The date YYYYMMDD of waterfall report.')
349  parser.add_argument(
350      '--email',
351      dest='email',
352      default='',
353      help='Email address to use for sending the report.')
354  parser.add_argument(
355      '--chromeos_root',
356      dest='chromeos_root',
357      required=True,
358      help='Chrome OS root in which to run chroot commands.')
359
360  options = parser.parse_args(argv)
361
362  if not ValidOptions(parser, options):
363    return 1
364
365  main_only = options.main
366  rotating_only = options.rotating
367  date = options.date
368
369  prod_access = CheckProdAccess()
370  if not prod_access:
371    print('ERROR: Please run prodaccess first.')
372    return
373
374  waterfall_report_dict = dict()
375  rotating_report_dict = dict()
376
377  ce = command_executer.GetCommandExecuter()
378  if not rotating_only:
379    GetMainWaterfallData(date, waterfall_report_dict, options.chromeos_root, ce)
380
381  if not main_only:
382    rotating_builds_dict = dict()
383    GetTryjobData(date, rotating_builds_dict)
384    if len(rotating_builds_dict.keys()) > 0:
385      for board in rotating_builds_dict.keys():
386        buildbucket_id = rotating_builds_dict[board]
387        GetRotatingBuildData(date, rotating_report_dict, options.chromeos_root,
388                             board, buildbucket_id, ce)
389
390  if options.email:
391    email_to = options.email
392  else:
393    email_to = getpass.getuser()
394
395  if waterfall_report_dict and not rotating_only:
396    main_report = GenerateWaterfallReport(waterfall_report_dict, 'main', date)
397
398    EmailReport(main_report, 'Main', format_date(date), email_to)
399    shutil.copy(main_report, ARCHIVE_DIR)
400  if rotating_report_dict and not main_only:
401    rotating_report = GenerateWaterfallReport(rotating_report_dict, 'rotating',
402                                              date)
403
404    EmailReport(rotating_report, 'Rotating', format_date(date), email_to)
405    shutil.copy(rotating_report, ARCHIVE_DIR)
406
407
408if __name__ == '__main__':
409  Main(sys.argv[1:])
410  sys.exit(0)
411