buildbot_utils.py revision 097419758febae084477a01fd92d678218b578e5
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                   async=False):
188  """Launch buildbot and get resulting trybot artifact name.
189
190    This function launches a buildbot with the appropriate flags to
191    build the test ChromeOS image, with the current ToT mobile compiler.  It
192    checks every 10 minutes to see if the trybot has finished.  When the trybot
193    has finished, it parses the resulting report logs to find the trybot
194    artifact (if one was created), and returns that artifact name.
195
196    chromeos_root is the path to the ChromeOS root, needed for finding chromite
197    and launching the buildbot.
198
199    buildbot_name is the name of the buildbot queue, such as lumpy-release or
200    daisy-paladin.
201
202    patch_list a python list of the patches, if any, for the buildbot to use.
203
204    build_tag is a (unique) string to be used to look up the buildbot results
205    from among all the build records.
206    """
207  ce = command_executer.GetCommandExecuter()
208  cbuildbot_path = os.path.join(chromeos_root, 'chromite/cbuildbot')
209  base_dir = os.getcwd()
210  patch_arg = ''
211  if patch_list:
212    for p in patch_list:
213      patch_arg = patch_arg + ' -g ' + repr(p)
214  toolchain_flags = ''
215  if build_toolchain:
216    toolchain_flags += '--latest-toolchain'
217  os.chdir(cbuildbot_path)
218  if other_flags:
219    optional_flags = ' '.join(other_flags)
220  else:
221    optional_flags = ''
222
223  # Launch buildbot with appropriate flags.
224  build = buildbot_name
225  description = build_tag
226  command_prefix = ''
227  if not patch_arg:
228    command_prefix = 'yes | '
229  command = ('%s ./cbuildbot --remote --nochromesdk %s'
230             ' --remote-description=%s %s %s %s' % (command_prefix,
231                                                    optional_flags, description,
232                                                    toolchain_flags, patch_arg,
233                                                    build))
234  _, out, _ = ce.RunCommandWOutput(command)
235  if 'Tryjob submitted!' not in out:
236    logger.GetLogger().LogFatal('Error occurred while launching trybot job: '
237                                '%s' % command)
238
239  if async:
240    # Do not wait for trybot job to finish; return immediately
241    return 0
242
243  os.chdir(base_dir)
244
245  build_id = 0
246  build_status = None
247  # Wait for  buildbot to finish running (check every 10 minutes).  Wait
248  # 10 minutes before the first check to give the buildbot time to launch
249  # (so we don't start looking for build data before it's out there).
250  time.sleep(SLEEP_TIME)
251  done = False
252  pending = True
253  # pending_time is the time between when we submit the job and when the
254  # buildbot actually launches the build.  running_time is the time between
255  # when the buildbot job launches and when it finishes.  The job is
256  # considered 'pending' until we can find an entry for it in the buildbot
257  # logs.
258  pending_time = SLEEP_TIME
259  running_time = 0
260  while not done:
261    done = True
262    build_info = GetBuildInfo(base_dir, build)
263    if not build_info:
264      if pending_time > TIME_OUT:
265        logger.GetLogger().LogFatal('Unable to get build logs for target %s.' %
266                                    build)
267      else:
268        pending_message = 'Unable to find build log; job may be pending.'
269        done = False
270
271    if done:
272      data_dict = FindBuildRecordFromLog(description, build_info)
273      if not data_dict:
274        # Trybot job may be pending (not actually launched yet).
275        if pending_time > TIME_OUT:
276          logger.GetLogger().LogFatal('Unable to find build record for trybot'
277                                      ' %s.' % description)
278        else:
279          pending_message = 'Unable to find build record; job may be pending.'
280          done = False
281
282      else:
283        # Now that we have actually found the entry for the build
284        # job in the build log, we know the job is actually
285        # runnning, not pending, so we flip the 'pending' flag.  We
286        # still have to wait for the buildbot job to finish running
287        # however.
288        pending = False
289        if 'True' in data_dict['completed']:
290          build_id = data_dict['number']
291          build_status = int(data_dict['result'])
292        else:
293          done = False
294
295    if not done:
296      if pending:
297        logger.GetLogger().LogOutput(pending_message)
298        logger.GetLogger().LogOutput('Current pending time: %d minutes.' %
299                                     (pending_time / 60))
300        pending_time += SLEEP_TIME
301      else:
302        logger.GetLogger().LogOutput('{0} minutes passed.'.format(running_time /
303                                                                  60))
304        logger.GetLogger().LogOutput('Sleeping {0} seconds.'.format(SLEEP_TIME))
305        running_time += SLEEP_TIME
306
307      time.sleep(SLEEP_TIME)
308      if running_time > TIME_OUT:
309        done = True
310
311  trybot_image = ''
312
313  if build.endswith('-toolchain'):
314    # For rotating testers, we don't care about their build_status
315    # result, because if any HWTest failed it will be non-zero.
316    trybot_image = FindArchiveImage(chromeos_root, build, build_id)
317  else:
318    # The nightly performance tests do not run HWTests, so if
319    # their build_status is non-zero, we do care.  In this case
320    # non-zero means the image itself probably did not build.
321    if build_status in OK_STATUS:
322      trybot_image = FindArchiveImage(chromeos_root, build, build_id)
323  if not trybot_image:
324    logger.GetLogger().LogError('Trybot job %s failed with status %d;'
325                                ' no trybot image generated.' %
326                                (description, build_status))
327
328  logger.GetLogger().LogOutput("trybot_image is '%s'" % trybot_image)
329  logger.GetLogger().LogOutput('build_status is %d' % build_status)
330  return trybot_image
331
332
333def DoesImageExist(chromeos_root, build):
334  """Check if the image for the given build exists."""
335
336  ce = command_executer.GetCommandExecuter()
337  command = ('gsutil ls gs://chromeos-image-archive/%s'
338             '/chromiumos_test_image.tar.xz' % (build))
339  ret = ce.ChrootRunCommand(chromeos_root, command, print_to_console=False)
340  return not ret
341
342
343def WaitForImage(chromeos_root, build):
344  """Wait for an image to be ready."""
345
346  elapsed_time = 0
347  while elapsed_time < TIME_OUT:
348    if DoesImageExist(chromeos_root, build):
349      return
350    logger.GetLogger().LogOutput('Image %s not ready, waiting for 10 minutes' %
351                                 build)
352    time.sleep(SLEEP_TIME)
353    elapsed_time += SLEEP_TIME
354
355  logger.GetLogger().LogOutput('Image %s not found, waited for %d hours' %
356                               (build, (TIME_OUT / 3600)))
357  raise BuildbotTimeout('Timeout while waiting for image %s' % build)
358