buildbot_utils.py revision 6c438e04d12f5dd94dd060c9ab41a61766dc313e
1# Copyright 2014 Google Inc. All Rights Reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Utilities for launching and accessing ChromeOS buildbots."""
5
6from __future__ import print_function
7
8import os
9import time
10import urllib2
11
12from cros_utils import command_executer
13from cros_utils import logger
14from cros_utils import buildbot_json
15
16SLEEP_TIME = 600  # 10 minutes; time between polling of buildbot.
17TIME_OUT = 28800  # Decide the build is dead or will never finish
18# after this time (8 hours).
19OK_STATUS = [  # List of result status values that are 'ok'.
20    # This was obtained from:
21    #   https://chromium.googlesource.com/chromium/tools/build/+/
22    #       master/third_party/buildbot_8_4p1/buildbot/status/results.py
23    0,  # "success"
24    1,  # "warnings"
25    6,  # "retry"
26]
27
28
29class BuildbotTimeout(Exception):
30  """Exception to throw when a buildbot operation timesout."""
31  pass
32
33
34def ParseReportLog(url, build):
35  """Scrape the trybot image name off the Reports log page.
36
37    This takes the URL for a trybot Reports Stage web page,
38    and a trybot build type, such as 'daisy-release'.  It
39    opens the web page and parses it looking for the trybot
40    artifact name (e.g. something like
41    'trybot-daisy-release/R40-6394.0.0-b1389'). It returns the
42    artifact name, if found.
43    """
44  trybot_image = ''
45  url += '/text'
46  newurl = url.replace('uberchromegw', 'chromegw')
47  webpage = urllib2.urlopen(newurl)
48  data = webpage.read()
49  lines = data.split('\n')
50  for l in lines:
51    if l.find('Artifacts') > 0 and l.find('trybot') > 0:
52      trybot_name = 'trybot-%s' % build
53      start_pos = l.find(trybot_name)
54      end_pos = l.find('@https://storage')
55      trybot_image = l[start_pos:end_pos]
56
57  return trybot_image
58
59
60def GetBuildData(buildbot_queue, build_id):
61  """Find the Reports stage web page for a trybot build.
62
63    This takes the name of a buildbot_queue, such as 'daisy-release'
64    and a build id (the build number), and uses the json buildbot api to
65    find the Reports stage web page for that build, if it exists.
66    """
67  builder = buildbot_json.Buildbot(
68      'http://chromegw/p/tryserver.chromiumos/').builders[buildbot_queue]
69  build_data = builder.builds[build_id].data
70  logs = build_data['logs']
71  for l in logs:
72    fname = l[1]
73    if 'steps/Report/' in fname:
74      return fname
75
76  return ''
77
78
79def FindBuildRecordFromLog(description, log_info):
80  """Find the right build record in the build logs.
81
82    Get the first build record from build log with a reason field
83    that matches 'description'. ('description' is a special tag we
84    created when we launched the buildbot, so we could find it at this
85    point.)
86    """
87
88  current_line = 1
89  while current_line < len(log_info):
90    my_dict = {}
91    # Read all the lines from one "Build" to the next into my_dict
92    while True:
93      key = log_info[current_line].split(':')[0].strip()
94      value = log_info[current_line].split(':', 1)[1].strip()
95      my_dict[key] = value
96      current_line += 1
97      if 'Build' in key or current_line == len(log_info):
98        break
99    try:
100      # Check to see of the build record is the right one.
101      if str(description) in my_dict['reason']:
102        # We found a match; we're done.
103        return my_dict
104    except KeyError:
105      print("reason is not in dictionary: '%s'" % repr(my_dict))
106    else:
107      # Keep going.
108      continue
109
110  # We hit the bottom of the log without a match.
111  return {}
112
113
114def GetBuildInfo(file_dir, builder):
115  """Get all the build records for the trybot builds.
116
117    file_dir is the toolchain_utils directory.
118    """
119  ce = command_executer.GetCommandExecuter()
120  commands = ('{0}/cros_utils/buildbot_json.py builds '
121              'http://chromegw/i/tryserver.chromiumos/'.format(file_dir))
122
123  if builder:
124    # For release builds, get logs from the 'release' builder.
125    if builder.endswith('-release'):
126      commands += ' -b release'
127    elif builder.endswith('-gcc-toolchain'):
128      commands += ' -b gcc_toolchain'
129    elif builder.endswith('-llvm-toolchain'):
130      commands += ' -b llvm_toolchain'
131    elif builder.endswith('-toolchain'):
132      commands += ' -b etc'
133    else:
134      commands += ' -b %s' % builder
135  _, buildinfo, _ = ce.RunCommandWOutput(commands, print_to_console=False)
136  build_log = buildinfo.splitlines()
137  return build_log
138
139
140def FindArchiveImage(chromeos_root, build, build_id):
141  """Returns name of the trybot artifact for board/build_id."""
142  ce = command_executer.GetCommandExecuter()
143  command = ('gsutil ls gs://chromeos-image-archive/trybot-%s/*b%s'
144             '/chromiumos_test_image.tar.xz' % (build, build_id))
145  _, out, _ = ce.ChrootRunCommandWOutput(
146      chromeos_root, command, print_to_console=False)
147  #
148  # If build_id is not unique, there may be multiple archive images
149  # to choose from; sort them & pick the first (newest).
150  #
151  # If there are multiple archive images found, out will look something
152  # like this:
153  #
154  # 'gs://.../R35-5692.0.0-b105/chromiumos_test_image.tar.xz
155  #  gs://.../R46-7339.0.0-b105/chromiumos_test_image.tar.xz'
156  #
157  out = out.rstrip('\n')
158  tmp_list = out.split('\n')
159  # After stripping the final '\n' and splitting on any other '\n', we get
160  # something like this:
161  #  tmp_list = [ 'gs://.../R35-5692.0.0-b105/chromiumos_test_image.tar.xz' ,
162  #               'gs://.../R46-7339.0.0-b105/chromiumos_test_image.tar.xz' ]
163  #
164  #  If we sort this in descending order, we should end up with the most
165  #  recent test image first, so that's what we do here.
166  #
167  if len(tmp_list) > 1:
168    tmp_list = sorted(tmp_list, reverse=True)
169  out = tmp_list[0]
170
171  trybot_image = ''
172  trybot_name = 'trybot-%s' % build
173  if out and out.find(trybot_name) > 0:
174    start_pos = out.find(trybot_name)
175    end_pos = out.find('/chromiumos_test_image')
176    trybot_image = out[start_pos:end_pos]
177
178  return trybot_image
179
180
181def GetTrybotImage(chromeos_root,
182                   buildbot_name,
183                   patch_list,
184                   build_tag,
185                   other_flags=[],
186                   build_toolchain=False):
187  """Launch buildbot and get resulting trybot artifact name.
188
189    This function launches a buildbot with the appropriate flags to
190    build the test ChromeOS image, with the current ToT mobile compiler.  It
191    checks every 10 minutes to see if the trybot has finished.  When the trybot
192    has finished, it parses the resulting report logs to find the trybot
193    artifact (if one was created), and returns that artifact name.
194
195    chromeos_root is the path to the ChromeOS root, needed for finding chromite
196    and launching the buildbot.
197
198    buildbot_name is the name of the buildbot queue, such as lumpy-release or
199    daisy-paladin.
200
201    patch_list a python list of the patches, if any, for the buildbot to use.
202
203    build_tag is a (unique) string to be used to look up the buildbot results
204    from among all the build records.
205    """
206  ce = command_executer.GetCommandExecuter()
207  cbuildbot_path = os.path.join(chromeos_root, 'chromite/cbuildbot')
208  base_dir = os.getcwd()
209  patch_arg = ''
210  if patch_list:
211    for p in patch_list:
212      patch_arg = patch_arg + ' -g ' + repr(p)
213  toolchain_flags = ''
214  if build_toolchain:
215    toolchain_flags += '--latest-toolchain'
216  os.chdir(cbuildbot_path)
217  if other_flags:
218    optional_flags = ' '.join(other_flags)
219  else:
220    optional_flags = ''
221
222  # Launch buildbot with appropriate flags.
223  build = buildbot_name
224  description = build_tag
225  command_prefix = ''
226  if not patch_arg:
227    command_prefix = 'yes | '
228  command = ('%s ./cbuildbot --remote --nochromesdk %s'
229             ' --remote-description=%s %s %s %s' % (command_prefix,
230                                                    optional_flags, description,
231                                                    toolchain_flags, patch_arg,
232                                                    build))
233  _, out, _ = ce.RunCommandWOutput(command)
234  if 'Tryjob submitted!' not in out:
235    logger.GetLogger().LogFatal('Error occurred while launching trybot job: '
236                                '%s' % command)
237  os.chdir(base_dir)
238
239  build_id = 0
240  build_status = None
241  # Wait for  buildbot to finish running (check every 10 minutes).  Wait
242  # 10 minutes before the first check to give the buildbot time to launch
243  # (so we don't start looking for build data before it's out there).
244  time.sleep(SLEEP_TIME)
245  done = False
246  pending = True
247  # pending_time is the time between when we submit the job and when the
248  # buildbot actually launches the build.  running_time is the time between
249  # when the buildbot job launches and when it finishes.  The job is
250  # considered 'pending' until we can find an entry for it in the buildbot
251  # logs.
252  pending_time = SLEEP_TIME
253  running_time = 0
254  while not done:
255    done = True
256    build_info = GetBuildInfo(base_dir, build)
257    if not build_info:
258      if pending_time > TIME_OUT:
259        logger.GetLogger().LogFatal('Unable to get build logs for target %s.' %
260                                    build)
261      else:
262        pending_message = 'Unable to find build log; job may be pending.'
263        done = False
264
265    if done:
266      data_dict = FindBuildRecordFromLog(description, build_info)
267      if not data_dict:
268        # Trybot job may be pending (not actually launched yet).
269        if pending_time > TIME_OUT:
270          logger.GetLogger().LogFatal('Unable to find build record for trybot'
271                                      ' %s.' % description)
272        else:
273          pending_message = 'Unable to find build record; job may be pending.'
274          done = False
275
276      else:
277        # Now that we have actually found the entry for the build
278        # job in the build log, we know the job is actually
279        # runnning, not pending, so we flip the 'pending' flag.  We
280        # still have to wait for the buildbot job to finish running
281        # however.
282        pending = False
283        if 'True' in data_dict['completed']:
284          build_id = data_dict['number']
285          build_status = int(data_dict['result'])
286        else:
287          done = False
288
289    if not done:
290      if pending:
291        logger.GetLogger().LogOutput(pending_message)
292        logger.GetLogger().LogOutput('Current pending time: %d minutes.' %
293                                     (pending_time / 60))
294        pending_time += SLEEP_TIME
295      else:
296        logger.GetLogger().LogOutput('{0} minutes passed.'.format(running_time /
297                                                                  60))
298        logger.GetLogger().LogOutput('Sleeping {0} seconds.'.format(SLEEP_TIME))
299        running_time += SLEEP_TIME
300
301      time.sleep(SLEEP_TIME)
302      if running_time > TIME_OUT:
303        done = True
304
305  trybot_image = ''
306
307  if build.endswith('-toolchain'):
308    # For rotating testers, we don't care about their build_status
309    # result, because if any HWTest failed it will be non-zero.
310    trybot_image = FindArchiveImage(chromeos_root, build, build_id)
311  else:
312    # The nightly performance tests do not run HWTests, so if
313    # their build_status is non-zero, we do care.  In this case
314    # non-zero means the image itself probably did not build.
315    if build_status in OK_STATUS:
316      trybot_image = FindArchiveImage(chromeos_root, build, build_id)
317  if not trybot_image:
318    logger.GetLogger().LogError('Trybot job %s failed with status %d;'
319                                ' no trybot image generated.' %
320                                (description, build_status))
321
322  logger.GetLogger().LogOutput("trybot_image is '%s'" % trybot_image)
323  logger.GetLogger().LogOutput('build_status is %d' % build_status)
324  return trybot_image
325
326
327def DoesImageExist(chromeos_root, build):
328  """Check if the image for the given build exists."""
329
330  ce = command_executer.GetCommandExecuter()
331  command = ('gsutil ls gs://chromeos-image-archive/%s'
332             '/chromiumos_test_image.tar.xz' % (build))
333  ret = ce.ChrootRunCommand(chromeos_root, command, print_to_console=False)
334  return not ret
335
336
337def WaitForImage(chromeos_root, build):
338  """Wait for an image to be ready."""
339
340  elapsed_time = 0
341  while elapsed_time < TIME_OUT:
342    if DoesImageExist(chromeos_root, build):
343      return
344    logger.GetLogger().LogOutput('Image %s not ready, waiting for 10 minutes' %
345                                 build)
346    time.sleep(SLEEP_TIME)
347    elapsed_time += SLEEP_TIME
348
349  logger.GetLogger().LogOutput('Image %s not found, waited for %d hours' %
350                               (build, (TIME_OUT / 3600)))
351  raise BuildbotTimeout('Timeout while waiting for image %s' % build)
352