OLD | NEW |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Post a try job to the Try server to produce build. It communicates | 5 """Post a try job request via HTTP to the Tryserver to produce build.""" |
6 to server by directly connecting via HTTP. | |
7 """ | |
8 | 6 |
9 import getpass | 7 import getpass |
| 8 import json |
10 import optparse | 9 import optparse |
11 import os | 10 import os |
12 import sys | 11 import sys |
13 import urllib | 12 import urllib |
14 import urllib2 | 13 import urllib2 |
15 | 14 |
| 15 # Link to get JSON data of builds |
| 16 BUILDER_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 |
| 20 BUILDER_HTML_URL = ('%(server_url)s/builders/%(bot_name)s/builds/%(build_num)s') |
| 21 |
| 22 # Tryserver buildbots status page |
| 23 TRY_SERVER_URL = 'http://build.chromium.org/p/tryserver.chromium' |
| 24 |
| 25 # Hostname of the tryserver where perf bisect builders are hosted. This is used |
| 26 # for posting build request to tryserver. |
| 27 BISECT_BUILDER_HOST = 'master4.golo.chromium.org' |
| 28 # 'try_job_port' on tryserver to post build request. |
| 29 BISECT_BUILDER_PORT = '8328' |
| 30 |
| 31 |
| 32 # From buildbot.status.builder. |
| 33 # See: http://docs.buildbot.net/current/developer/results.html |
| 34 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, TRYPENDING = range(7) |
| 35 |
| 36 # Status codes that can be returned by the GetBuildStatus method. |
| 37 OK = (SUCCESS, WARNINGS) |
| 38 # Indicates build failure. |
| 39 FAILED = (FAILURE, EXCEPTION, SKIPPED) |
| 40 # Inidcates build in progress or in pending queue. |
| 41 PENDING = (RETRY, TRYPENDING) |
| 42 |
16 | 43 |
17 class ServerAccessError(Exception): | 44 class ServerAccessError(Exception): |
| 45 |
18 def __str__(self): | 46 def __str__(self): |
19 return '%s\nSorry, cannot connect to server.' % self.args[0] | 47 return '%s\nSorry, cannot connect to server.' % self.args[0] |
20 | 48 |
21 | 49 |
22 def PostTryJob(url_params): | 50 def PostTryJob(url_params): |
23 """Sends a build request to the server using the HTTP protocol. | 51 """Sends a build request to the server using the HTTP protocol. |
24 | 52 |
25 Args: | 53 Args: |
26 url_params: A dictionary of query parameters to be sent in the request. | 54 url_params: A dictionary of query parameters to be sent in the request. |
27 In order to post build request to try server, this dictionary | 55 In order to post build request to try server, this dictionary |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
61 raise ServerAccessError('%s is unaccessible. Reason: %s' % (url, e)) | 89 raise ServerAccessError('%s is unaccessible. Reason: %s' % (url, e)) |
62 if not connection: | 90 if not connection: |
63 raise ServerAccessError('%s is unaccessible.' % url) | 91 raise ServerAccessError('%s is unaccessible.' % url) |
64 response = connection.read() | 92 response = connection.read() |
65 print 'Received %s from server' % response | 93 print 'Received %s from server' % response |
66 if response != 'OK': | 94 if response != 'OK': |
67 raise ServerAccessError('%s is unaccessible. Got:\n%s' % (url, response)) | 95 raise ServerAccessError('%s is unaccessible. Got:\n%s' % (url, response)) |
68 return True | 96 return True |
69 | 97 |
70 | 98 |
| 99 def _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 |
| 117 def _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 |
| 138 def _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 |
| 164 def _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 |
| 188 def _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 |
| 203 def _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 |
| 224 def 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 # Gets the buildbot url for the given host and port. |
| 238 server_url = _GetBuildBotUrl(builder_host, builder_port) |
| 239 buildbot_url = BUILDER_JSON_URL % {'server_url': server_url, |
| 240 'bot_name': bot_name, |
| 241 'build_num': build_num |
| 242 } |
| 243 build_data = _GetBuildData(buildbot_url) |
| 244 results_url = None |
| 245 if build_data: |
| 246 # Link to build on the buildbot showing status of build steps. |
| 247 results_url = BUILDER_HTML_URL % {'server_url': server_url, |
| 248 'bot_name': bot_name, |
| 249 'build_num': build_num |
| 250 } |
| 251 if _IsBuildFailed(build_data): |
| 252 return (FAILED, results_url) |
| 253 |
| 254 elif _IsBuildSuccessful(build_data): |
| 255 return (OK, results_url) |
| 256 return (PENDING, results_url) |
| 257 |
| 258 |
| 259 def GetBuildNumFromBuilder(build_reason, bot_name, builder_host, builder_port): |
| 260 """Gets build number on build status page for a given build reason. |
| 261 |
| 262 It parses the JSON data from buildbot page and collect basic information |
| 263 about the all the builds and then this uniquely identifies the build based |
| 264 on the 'reason' attribute in builds's JSON data. |
| 265 The 'reason' attribute set while a build request is posted, and same is used |
| 266 to identify the build on status page. |
| 267 |
| 268 Args: |
| 269 build_reason: A unique build name set to build on tryserver. |
| 270 bot_name: Name of the bot where the build information is scanned. |
| 271 builder_host: Hostname of the server where the builder is hosted. |
| 272 builder_port: Port number of ther server where the builder is hosted. |
| 273 |
| 274 Returns: |
| 275 A build number as a string if found, otherwise None. |
| 276 """ |
| 277 # Gets the buildbot url for the given host and port. |
| 278 server_url = _GetBuildBotUrl(builder_host, builder_port) |
| 279 buildbot_url = BUILDER_JSON_URL % {'server_url': server_url, |
| 280 'bot_name': bot_name, |
| 281 'build_num': '_all' |
| 282 } |
| 283 builds_json = _FetchBuilderData(buildbot_url) |
| 284 if builds_json: |
| 285 builds_data = json.loads(builds_json) |
| 286 for current_build in builds_data: |
| 287 if builds_data[current_build].get('reason') == build_reason: |
| 288 return builds_data[current_build].get('number') |
| 289 return None |
| 290 |
| 291 |
71 def _GetQueryParams(options): | 292 def _GetQueryParams(options): |
72 """Parses common query parameters which will be passed to PostTryJob. | 293 """Parses common query parameters which will be passed to PostTryJob. |
73 | 294 |
74 Args: | 295 Args: |
75 options: The options object parsed from the command line. | 296 options: The options object parsed from the command line. |
76 | 297 |
77 Returns: | 298 Returns: |
78 A dictionary consists of query parameters. | 299 A dictionary consists of query parameters. |
79 """ | 300 """ |
80 values = {'host': options.host, | 301 values = {'host': options.host, |
(...skipping 13 matching lines...) Expand all Loading... |
94 values['patch'] = options.patch | 315 values['patch'] = options.patch |
95 return values | 316 return values |
96 | 317 |
97 | 318 |
98 def _GenParser(): | 319 def _GenParser(): |
99 """Parses the command line for posting build request.""" | 320 """Parses the command line for posting build request.""" |
100 usage = ('%prog [options]\n' | 321 usage = ('%prog [options]\n' |
101 'Post a build request to the try server for the given revision.\n') | 322 'Post a build request to the try server for the given revision.\n') |
102 parser = optparse.OptionParser(usage=usage) | 323 parser = optparse.OptionParser(usage=usage) |
103 parser.add_option('-H', '--host', | 324 parser.add_option('-H', '--host', |
104 help='Host address of the try server.') | 325 help='Host address of the try server.') |
105 parser.add_option('-P', '--port', type='int', | 326 parser.add_option('-P', '--port', type='int', |
106 help='HTTP port of the try server.') | 327 help='HTTP port of the try server.') |
107 parser.add_option('-u', '--user', default=getpass.getuser(), | 328 parser.add_option('-u', '--user', default=getpass.getuser(), |
108 dest='user', | 329 dest='user', |
109 help='Owner user name [default: %default]') | 330 help='Owner user name [default: %default]') |
110 parser.add_option('-e', '--email', | 331 parser.add_option('-e', '--email', |
111 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', | 332 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', |
112 os.environ.get('EMAIL_ADDRESS')), | 333 os.environ.get('EMAIL_ADDRESS')), |
113 help='Email address where to send the results. Use either ' | 334 help=('Email address where to send the results. Use either ' |
114 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment ' | 335 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment ' |
115 'variable or EMAIL_ADDRESS to set the email address ' | 336 'variable or EMAIL_ADDRESS to set the email address ' |
116 'the try bots report results to [default: %default]') | 337 'the try bots report results to [default: %default]')) |
117 parser.add_option('-n', '--name', | 338 parser.add_option('-n', '--name', |
118 default= 'try_job_http', | 339 default='try_job_http', |
119 help='Descriptive name of the try job') | 340 help='Descriptive name of the try job') |
120 parser.add_option('-b', '--bot', | 341 parser.add_option('-b', '--bot', |
121 help=('IMPORTANT: specify ONE builder per run is supported.' | 342 help=('IMPORTANT: specify ONE builder per run is supported.' |
122 'Run script for each builders separately.')) | 343 'Run script for each builders separately.')) |
123 parser.add_option('-r', '--revision', | 344 parser.add_option('-r', '--revision', |
124 help='Revision to use for the try job; default: the ' | 345 help=('Revision to use for the try job; default: the ' |
125 'revision will be determined by the try server; see ' | 346 'revision will be determined by the try server; see ' |
126 'its waterfall for more info') | 347 'its waterfall for more info')) |
127 parser.add_option('--root', | 348 parser.add_option('--root', |
128 help='Root to use for the patch; base subdirectory for ' | 349 help=('Root to use for the patch; base subdirectory for ' |
129 'patch created in a subdirectory') | 350 'patch created in a subdirectory')) |
130 parser.add_option('--patch', | 351 parser.add_option('--patch', |
131 help='Patch information.') | 352 help='Patch information.') |
132 return parser | 353 return parser |
133 | 354 |
134 | 355 |
135 def Main(argv): | 356 def Main(argv): |
136 parser = _GenParser() | 357 parser = _GenParser() |
137 options, args = parser.parse_args() | 358 options, _ = parser.parse_args() |
138 if not options.host: | 359 if not options.host: |
139 raise ServerAccessError('Please use the --host option to specify the try ' | 360 raise ServerAccessError('Please use the --host option to specify the try ' |
140 'server host to connect to.') | 361 'server host to connect to.') |
141 if not options.port: | 362 if not options.port: |
142 raise ServerAccessError('Please use the --port option to specify the try ' | 363 raise ServerAccessError('Please use the --port option to specify the try ' |
143 'server port to connect to.') | 364 'server port to connect to.') |
144 params = _GetQueryParams(options) | 365 params = _GetQueryParams(options) |
145 PostTryJob(params) | 366 PostTryJob(params) |
146 | 367 |
147 | 368 |
148 if __name__ == '__main__': | 369 if __name__ == '__main__': |
149 sys.exit(Main(sys.argv)) | 370 sys.exit(Main(sys.argv)) |
| 371 |
OLD | NEW |