• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 The Chromium Authors. 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
5"""Post a try job request via HTTP to the Tryserver to produce build."""
6
7import getpass
8import json
9import optparse
10import os
11import sys
12import urllib
13import urllib2
14
15# Link to get JSON data of builds
16BUILDER_JSON_URL = ('%(server_url)s/json/builders/%(bot_name)s/builds/'
17                    '%(build_num)s?as_text=1&filter=0')
18
19# Link to display build steps
20BUILDER_HTML_URL = ('%(server_url)s/builders/%(bot_name)s/builds/%(build_num)s')
21
22# Tryserver buildbots status page
23TRY_SERVER_URL = 'http://build.chromium.org/p/tryserver.chromium.perf'
24
25# Hostname of the tryserver where perf bisect builders are hosted. This is used
26# for posting build request to tryserver.
27BISECT_BUILDER_HOST = 'master4.golo.chromium.org'
28# 'try_job_port' on tryserver to post build request.
29BISECT_BUILDER_PORT = 8341
30
31
32# From buildbot.status.builder.
33# See: http://docs.buildbot.net/current/developer/results.html
34SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, TRYPENDING = range(7)
35
36# Status codes that can be returned by the GetBuildStatus method.
37OK = (SUCCESS, WARNINGS)
38# Indicates build failure.
39FAILED = (FAILURE, EXCEPTION, SKIPPED)
40# Inidcates build in progress or in pending queue.
41PENDING = (RETRY, TRYPENDING)
42
43
44class ServerAccessError(Exception):
45
46  def __str__(self):
47    return '%s\nSorry, cannot connect to server.' % self.args[0]
48
49
50def PostTryJob(url_params):
51  """Sends a build request to the server using the HTTP protocol.
52
53  Args:
54    url_params: A dictionary of query parameters to be sent in the request.
55                In order to post build request to try server, this dictionary
56                should contain information for following keys:
57                'host': Hostname of the try server.
58                'port': Port of the try server.
59                'revision': SVN Revision to build.
60                'bot': Name of builder bot which would be used.
61  Returns:
62    True if the request is posted successfully. Otherwise throws an exception.
63  """
64  # Parse url parameters to be sent to Try server.
65  if not url_params.get('host'):
66    raise ValueError('Hostname of server to connect is missing.')
67  if not url_params.get('port'):
68    raise ValueError('Port of server to connect is missing.')
69  if not url_params.get('revision'):
70    raise ValueError('Missing revision details. Please specify revision'
71                     ' information.')
72  if not url_params.get('bot'):
73    raise ValueError('Missing bot details. Please specify bot information.')
74
75  # Pop 'host' and 'port' to avoid passing them as query params.
76  url = 'http://%s:%s/send_try_patch' % (url_params.pop('host'),
77                                         url_params.pop('port'))
78
79  print 'Sending by HTTP'
80  query_params = '&'.join('%s=%s' % (k, v) for k, v in url_params.iteritems())
81  print 'url: %s?%s' % (url, query_params)
82
83  connection = None
84  try:
85    print 'Opening connection...'
86    connection = urllib2.urlopen(url, urllib.urlencode(url_params))
87    print 'Done, request sent to server to produce build.'
88  except IOError, e:
89    raise ServerAccessError('%s is unaccessible. Reason: %s' % (url, e))
90  if not connection:
91    raise ServerAccessError('%s is unaccessible.' % url)
92  response = connection.read()
93  print 'Received %s from server' % response
94  if response != 'OK':
95    raise ServerAccessError('%s is unaccessible. Got:\n%s' % (url, response))
96  return True
97
98
99def _IsBuildRunning(build_data):
100  """Checks whether the build is in progress on buildbot.
101
102  Presence of currentStep element in build JSON indicates build is in progress.
103
104  Args:
105    build_data: A dictionary with build data, loaded from buildbot JSON API.
106
107  Returns:
108    True if build is in progress, otherwise False.
109  """
110  current_step = build_data.get('currentStep')
111  if (current_step and current_step.get('isStarted') and
112      current_step.get('results') is None):
113    return True
114  return False
115
116
117def _IsBuildFailed(build_data):
118  """Checks whether the build failed on buildbot.
119
120  Sometime build status is marked as failed even though compile and packaging
121  steps are successful. This may happen due to some intermediate steps of less
122  importance such as gclient revert, generate_telemetry_profile are failed.
123  Therefore we do an addition check to confirm if build was successful by
124  calling _IsBuildSuccessful.
125
126  Args:
127    build_data: A dictionary with build data, loaded from buildbot JSON API.
128
129  Returns:
130    True if revision is failed build, otherwise False.
131  """
132  if (build_data.get('results') in FAILED and
133      not _IsBuildSuccessful(build_data)):
134    return True
135  return False
136
137
138def _IsBuildSuccessful(build_data):
139  """Checks whether the build succeeded on buildbot.
140
141  We treat build as successful if the package_build step is completed without
142  any error i.e., when results attribute of the this step has value 0 or 1
143  in its first element.
144
145  Args:
146    build_data: A dictionary with build data, loaded from buildbot JSON API.
147
148  Returns:
149    True if revision is successfully build, otherwise False.
150  """
151  if build_data.get('steps'):
152    for item in build_data.get('steps'):
153      # The 'results' attribute of each step consists of two elements,
154      # results[0]: This represents the status of build step.
155      # See: http://docs.buildbot.net/current/developer/results.html
156      # results[1]: List of items, contains text if step fails, otherwise empty.
157      if (item.get('name') == 'package_build' and
158          item.get('isFinished') and
159          item.get('results')[0] in OK):
160        return True
161  return False
162
163
164def _FetchBuilderData(builder_url):
165  """Fetches JSON data for the all the builds from the tryserver.
166
167  Args:
168    builder_url: A tryserver URL to fetch builds information.
169
170  Returns:
171    A dictionary with information of all build on the tryserver.
172  """
173  data = None
174  try:
175    url = urllib2.urlopen(builder_url)
176  except urllib2.URLError, e:
177    print ('urllib2.urlopen error %s, waterfall status page down.[%s]' % (
178        builder_url, str(e)))
179    return None
180  if url is not None:
181    try:
182      data = url.read()
183    except IOError, e:
184      print 'urllib2 file object read error %s, [%s].' % (builder_url, str(e))
185  return data
186
187
188def _GetBuildData(buildbot_url):
189  """Gets build information for the given build id from the tryserver.
190
191  Args:
192    buildbot_url: A tryserver URL to fetch build information.
193
194  Returns:
195    A dictionary with build information if build exists, otherwise None.
196  """
197  builds_json = _FetchBuilderData(buildbot_url)
198  if builds_json:
199    return json.loads(builds_json)
200  return None
201
202
203def _GetBuildBotUrl(builder_host, builder_port):
204  """Gets build bot URL based on the host and port of the builders.
205
206  Note: All bisect builder bots are hosted on tryserver.chromium i.e.,
207  on master4:8328, since we cannot access tryserver using host and port
208  number directly, we use tryserver URL.
209
210  Args:
211    builder_host: Hostname of the server where the builder is hosted.
212    builder_port: Port number of ther server where the builder is hosted.
213
214  Returns:
215    URL of the buildbot as a string.
216  """
217  if (builder_host == BISECT_BUILDER_HOST and
218      builder_port == BISECT_BUILDER_PORT):
219    return TRY_SERVER_URL
220  else:
221    return 'http://%s:%s' % (builder_host, builder_port)
222
223
224def GetBuildStatus(build_num, bot_name, builder_host, builder_port):
225  """Gets build status from the buildbot status page for a given build number.
226
227  Args:
228    build_num: A build number on tryserver to determine its status.
229    bot_name: Name of the bot where the build information is scanned.
230    builder_host: Hostname of the server where the builder is hosted.
231    builder_port: Port number of ther server where the builder is hosted.
232
233  Returns:
234    A tuple consists of build status (SUCCESS, FAILED or PENDING) and a link
235    to build status page on the waterfall.
236  """
237  results_url = None
238  if build_num:
239    # Gets the buildbot url for the given host and port.
240    server_url = _GetBuildBotUrl(builder_host, builder_port)
241    buildbot_url = BUILDER_JSON_URL % {'server_url': server_url,
242                                       'bot_name': bot_name,
243                                       'build_num': build_num
244                                      }
245    build_data = _GetBuildData(buildbot_url)
246    if build_data:
247      # Link to build on the buildbot showing status of build steps.
248      results_url = BUILDER_HTML_URL % {'server_url': server_url,
249                                        'bot_name': bot_name,
250                                        'build_num': build_num
251                                       }
252      if _IsBuildFailed(build_data):
253        return (FAILED, results_url)
254
255      elif _IsBuildSuccessful(build_data):
256        return (OK, results_url)
257  return (PENDING, results_url)
258
259
260def GetBuildNumFromBuilder(build_reason, bot_name, builder_host, builder_port):
261  """Gets build number on build status page for a given build reason.
262
263  It parses the JSON data from buildbot page and collect basic information
264  about the all the builds and then this uniquely identifies the build based
265  on the 'reason' attribute in builds's JSON data.
266  The 'reason' attribute set while a build request is posted, and same is used
267  to identify the build on status page.
268
269  Args:
270    build_reason: A unique build name set to build on tryserver.
271    bot_name: Name of the bot where the build information is scanned.
272    builder_host: Hostname of the server where the builder is hosted.
273    builder_port: Port number of ther server where the builder is hosted.
274
275  Returns:
276    A build number as a string if found, otherwise None.
277  """
278  # Gets the buildbot url for the given host and port.
279  server_url = _GetBuildBotUrl(builder_host, builder_port)
280  buildbot_url = BUILDER_JSON_URL % {'server_url': server_url,
281                                     'bot_name': bot_name,
282                                     'build_num': '_all'
283                                    }
284  builds_json = _FetchBuilderData(buildbot_url)
285  if builds_json:
286    builds_data = json.loads(builds_json)
287    for current_build in builds_data:
288      if builds_data[current_build].get('reason') == build_reason:
289        return builds_data[current_build].get('number')
290  return None
291
292
293def _GetQueryParams(options):
294  """Parses common query parameters which will be passed to PostTryJob.
295
296  Args:
297    options: The options object parsed from the command line.
298
299  Returns:
300    A dictionary consists of query parameters.
301  """
302  values = {'host': options.host,
303            'port': options.port,
304            'user': options.user,
305            'name': options.name
306           }
307  if options.email:
308    values['email'] = options.email
309  if options.revision:
310    values['revision'] = options.revision
311  if options.root:
312    values['root'] = options.root
313  if options.bot:
314    values['bot'] = options.bot
315  if options.patch:
316    values['patch'] = options.patch
317  return values
318
319
320def _GenParser():
321  """Parses the command line for posting build request."""
322  usage = ('%prog [options]\n'
323           'Post a build request to the try server for the given revision.\n')
324  parser = optparse.OptionParser(usage=usage)
325  parser.add_option('-H', '--host',
326                    help='Host address of the try server.')
327  parser.add_option('-P', '--port', type='int',
328                    help='HTTP port of the try server.')
329  parser.add_option('-u', '--user', default=getpass.getuser(),
330                    dest='user',
331                    help='Owner user name [default: %default]')
332  parser.add_option('-e', '--email',
333                    default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS',
334                                           os.environ.get('EMAIL_ADDRESS')),
335                    help=('Email address where to send the results. Use either '
336                          'the TRYBOT_RESULTS_EMAIL_ADDRESS environment '
337                          'variable or EMAIL_ADDRESS to set the email address '
338                          'the try bots report results to [default: %default]'))
339  parser.add_option('-n', '--name',
340                    default='try_job_http',
341                    help='Descriptive name of the try job')
342  parser.add_option('-b', '--bot',
343                    help=('IMPORTANT: specify ONE builder per run is supported.'
344                          'Run script for each builders separately.'))
345  parser.add_option('-r', '--revision',
346                    help=('Revision to use for the try job; default: the '
347                          'revision will be determined by the try server; see '
348                          'its waterfall for more info'))
349  parser.add_option('--root',
350                    help=('Root to use for the patch; base subdirectory for '
351                          'patch created in a subdirectory'))
352  parser.add_option('--patch',
353                    help='Patch information.')
354  return parser
355
356
357def Main(argv):
358  parser = _GenParser()
359  options, _ = parser.parse_args()
360  if not options.host:
361    raise ServerAccessError('Please use the --host option to specify the try '
362                            'server host to connect to.')
363  if not options.port:
364    raise ServerAccessError('Please use the --port option to specify the try '
365                            'server port to connect to.')
366  params = _GetQueryParams(options)
367  PostTryJob(params)
368
369
370if __name__ == '__main__':
371  sys.exit(Main(sys.argv))
372
373