blob: 99f393ac66efb57b5069ef70e0f5bfe644d7998f [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005
6"""Client tool to trigger tasks or retrieve results from a Swarming server."""
7
maruela9fe2cb2017-05-10 10:43:23 -07008__version__ = '0.9.1'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import subprocess
17import sys
maruel11e31af2017-02-15 07:30:50 -080018import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
iannucci31ab9192017-05-02 19:11:56 -070038import cipd
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040039import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000040import isolateserver
maruelc070e672016-02-22 17:32:57 -080041import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000042
43
tansella4949442016-06-23 22:34:32 -070044ROOT_DIR = os.path.dirname(os.path.abspath(
45 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050046
47
48class Failure(Exception):
49 """Generic failure."""
50 pass
51
52
maruela9fe2cb2017-05-10 10:43:23 -070053def default_task_name(options):
54 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050055 if not options.task_name:
maruela9fe2cb2017-05-10 10:43:23 -070056 task_name = u'%s/%s' % (
maruel4e901792017-05-09 12:07:02 -070057 options.user,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050058 '_'.join(
59 '%s=%s' % (k, v)
maruela9fe2cb2017-05-10 10:43:23 -070060 for k, v in sorted(options.dimensions.iteritems())))
61 if options.isolated:
62 task_name += u'/' + options.isolated
63 return task_name
64 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050065
66
67### Triggering.
68
69
maruel77f720b2015-09-15 12:35:22 -070070# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070071CipdPackage = collections.namedtuple(
72 'CipdPackage',
73 [
74 'package_name',
75 'path',
76 'version',
77 ])
78
79
80# See ../appengine/swarming/swarming_rpcs.py.
81CipdInput = collections.namedtuple(
82 'CipdInput',
83 [
84 'client_package',
85 'packages',
86 'server',
87 ])
88
89
90# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070091FilesRef = collections.namedtuple(
92 'FilesRef',
93 [
94 'isolated',
95 'isolatedserver',
96 'namespace',
97 ])
98
99
100# See ../appengine/swarming/swarming_rpcs.py.
101TaskProperties = collections.namedtuple(
102 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500103 [
maruel681d6802017-01-17 16:56:03 -0800104 'caches',
borenet02f772b2016-06-22 12:42:19 -0700105 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500106 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500107 'dimensions',
108 'env',
maruel77f720b2015-09-15 12:35:22 -0700109 'execution_timeout_secs',
110 'extra_args',
111 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500112 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700113 'inputs_ref',
114 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700115 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700116 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700117 ])
118
119
120# See ../appengine/swarming/swarming_rpcs.py.
121NewTaskRequest = collections.namedtuple(
122 'NewTaskRequest',
123 [
124 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500125 'name',
maruel77f720b2015-09-15 12:35:22 -0700126 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500127 'priority',
maruel77f720b2015-09-15 12:35:22 -0700128 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700129 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500130 'tags',
131 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500132 ])
133
134
maruel77f720b2015-09-15 12:35:22 -0700135def namedtuple_to_dict(value):
136 """Recursively converts a namedtuple to a dict."""
137 out = dict(value._asdict())
138 for k, v in out.iteritems():
139 if hasattr(v, '_asdict'):
140 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700141 elif isinstance(v, (list, tuple)):
142 l = []
143 for elem in v:
144 if hasattr(elem, '_asdict'):
145 l.append(namedtuple_to_dict(elem))
146 else:
147 l.append(elem)
148 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700149 return out
150
151
vadimsh93d167c2016-09-13 11:31:51 -0700152def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800153 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700154
155 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500156 """
maruel77f720b2015-09-15 12:35:22 -0700157 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700158 if hide_token:
159 if out['service_account_token'] not in (None, 'bot', 'none'):
160 out['service_account_token'] = '<hidden>'
161 # Don't send 'service_account_token' if it is None to avoid confusing older
162 # version of the server that doesn't know about 'service_account_token'.
163 if out['service_account_token'] in (None, 'none'):
164 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700165 # Maps are not supported until protobuf v3.
166 out['properties']['dimensions'] = [
167 {'key': k, 'value': v}
168 for k, v in out['properties']['dimensions'].iteritems()
169 ]
170 out['properties']['dimensions'].sort(key=lambda x: x['key'])
171 out['properties']['env'] = [
172 {'key': k, 'value': v}
173 for k, v in out['properties']['env'].iteritems()
174 ]
175 out['properties']['env'].sort(key=lambda x: x['key'])
176 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500177
178
maruel77f720b2015-09-15 12:35:22 -0700179def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500180 """Triggers a request on the Swarming server and returns the json data.
181
182 It's the low-level function.
183
184 Returns:
185 {
186 'request': {
187 'created_ts': u'2010-01-02 03:04:05',
188 'name': ..
189 },
190 'task_id': '12300',
191 }
192 """
193 logging.info('Triggering: %s', raw_request['name'])
194
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500195 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700196 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197 if not result:
198 on_error.report('Failed to trigger task %s' % raw_request['name'])
199 return None
maruele557bce2015-11-17 09:01:27 -0800200 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800201 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800202 msg = 'Failed to trigger task %s' % raw_request['name']
203 if result['error'].get('errors'):
204 for err in result['error']['errors']:
205 if err.get('message'):
206 msg += '\nMessage: %s' % err['message']
207 if err.get('debugInfo'):
208 msg += '\nDebug info:\n%s' % err['debugInfo']
209 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800210 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800211
212 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800213 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500214 return result
215
216
217def setup_googletest(env, shards, index):
218 """Sets googletest specific environment variables."""
219 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700220 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
221 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
222 env = env[:]
223 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
224 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500225 return env
226
227
228def trigger_task_shards(swarming, task_request, shards):
229 """Triggers one or many subtasks of a sharded task.
230
231 Returns:
232 Dict with task details, returned to caller as part of --dump-json output.
233 None in case of failure.
234 """
235 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700236 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500237 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700238 req['properties']['env'] = setup_googletest(
239 req['properties']['env'], shards, index)
240 req['name'] += ':%s:%s' % (index, shards)
241 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242
243 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500244 tasks = {}
245 priority_warning = False
246 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700247 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 if not task:
249 break
250 logging.info('Request result: %s', task)
251 if (not priority_warning and
252 task['request']['priority'] != task_request.priority):
253 priority_warning = True
254 print >> sys.stderr, (
255 'Priority was reset to %s' % task['request']['priority'])
256 tasks[request['name']] = {
257 'shard_index': index,
258 'task_id': task['task_id'],
259 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
260 }
261
262 # Some shards weren't triggered. Abort everything.
263 if len(tasks) != len(requests):
264 if tasks:
265 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
266 len(tasks), len(requests))
267 for task_dict in tasks.itervalues():
268 abort_task(swarming, task_dict['task_id'])
269 return None
270
271 return tasks
272
273
vadimsh93d167c2016-09-13 11:31:51 -0700274def mint_service_account_token(service_account):
275 """Given a service account name returns a delegation token for this account.
276
277 The token is generated based on triggering user's credentials. It is passed
278 to Swarming, that uses it when running tasks.
279 """
280 logging.info(
281 'Generating delegation token for service account "%s"', service_account)
282 raise NotImplementedError('Custom service accounts are not implemented yet')
283
284
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500285### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000286
287
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700288# How often to print status updates to stdout in 'collect'.
289STATUS_UPDATE_INTERVAL = 15 * 60.
290
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400291
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400292class State(object):
293 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000294
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400295 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
296 values are part of the API so if they change, the API changed.
297
298 It's in fact an enum. Values should be in decreasing order of importance.
299 """
300 RUNNING = 0x10
301 PENDING = 0x20
302 EXPIRED = 0x30
303 TIMED_OUT = 0x40
304 BOT_DIED = 0x50
305 CANCELED = 0x60
306 COMPLETED = 0x70
307
maruel77f720b2015-09-15 12:35:22 -0700308 STATES = (
309 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
310 'COMPLETED')
311 STATES_RUNNING = ('RUNNING', 'PENDING')
312 STATES_NOT_RUNNING = (
313 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
314 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
315 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400316
317 _NAMES = {
318 RUNNING: 'Running',
319 PENDING: 'Pending',
320 EXPIRED: 'Expired',
321 TIMED_OUT: 'Execution timed out',
322 BOT_DIED: 'Bot died',
323 CANCELED: 'User canceled',
324 COMPLETED: 'Completed',
325 }
326
maruel77f720b2015-09-15 12:35:22 -0700327 _ENUMS = {
328 'RUNNING': RUNNING,
329 'PENDING': PENDING,
330 'EXPIRED': EXPIRED,
331 'TIMED_OUT': TIMED_OUT,
332 'BOT_DIED': BOT_DIED,
333 'CANCELED': CANCELED,
334 'COMPLETED': COMPLETED,
335 }
336
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400337 @classmethod
338 def to_string(cls, state):
339 """Returns a user-readable string representing a State."""
340 if state not in cls._NAMES:
341 raise ValueError('Invalid state %s' % state)
342 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000343
maruel77f720b2015-09-15 12:35:22 -0700344 @classmethod
345 def from_enum(cls, state):
346 """Returns int value based on the string."""
347 if state not in cls._ENUMS:
348 raise ValueError('Invalid state %s' % state)
349 return cls._ENUMS[state]
350
maruel@chromium.org0437a732013-08-27 16:05:52 +0000351
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700352class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700353 """Assembles task execution summary (for --task-summary-json output).
354
355 Optionally fetches task outputs from isolate server to local disk (used when
356 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700357
358 This object is shared among multiple threads running 'retrieve_results'
359 function, in particular they call 'process_shard_result' method in parallel.
360 """
361
maruel0eb1d1b2015-10-02 14:48:21 -0700362 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
364
365 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700366 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700367 shard_count: expected number of task shards.
368 """
maruel12e30012015-10-09 11:55:35 -0700369 self.task_output_dir = (
370 unicode(os.path.abspath(task_output_dir))
371 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 self.shard_count = shard_count
373
374 self._lock = threading.Lock()
375 self._per_shard_results = {}
376 self._storage = None
377
nodire5028a92016-04-29 14:38:21 -0700378 if self.task_output_dir:
379 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700380
Vadim Shtayurab450c602014-05-12 19:23:25 -0700381 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 """Stores results of a single task shard, fetches output files if necessary.
383
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400384 Modifies |result| in place.
385
maruel77f720b2015-09-15 12:35:22 -0700386 shard_index is 0-based.
387
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700388 Called concurrently from multiple threads.
389 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700390 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700391 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700392 if shard_index < 0 or shard_index >= self.shard_count:
393 logging.warning(
394 'Shard index %d is outside of expected range: [0; %d]',
395 shard_index, self.shard_count - 1)
396 return
397
maruel77f720b2015-09-15 12:35:22 -0700398 if result.get('outputs_ref'):
399 ref = result['outputs_ref']
400 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
401 ref['isolatedserver'],
402 urllib.urlencode(
403 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400404
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700405 # Store result dict of that shard, ignore results we've already seen.
406 with self._lock:
407 if shard_index in self._per_shard_results:
408 logging.warning('Ignoring duplicate shard index %d', shard_index)
409 return
410 self._per_shard_results[shard_index] = result
411
412 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700413 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400414 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700415 result['outputs_ref']['isolatedserver'],
416 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400417 if storage:
418 # Output files are supposed to be small and they are not reused across
419 # tasks. So use MemoryCache for them instead of on-disk cache. Make
420 # files writable, so that calling script can delete them.
421 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700422 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400423 storage,
424 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700425 os.path.join(self.task_output_dir, str(shard_index)),
426 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427
428 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700429 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430 with self._lock:
431 # Write an array of shard results with None for missing shards.
432 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 'shards': [
434 self._per_shard_results.get(i) for i in xrange(self.shard_count)
435 ],
436 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700437 # Write summary.json to task_output_dir as well.
438 if self.task_output_dir:
439 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700440 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700441 summary,
442 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 if self._storage:
444 self._storage.close()
445 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700446 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447
448 def _get_storage(self, isolate_server, namespace):
449 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700450 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700451 with self._lock:
452 if not self._storage:
453 self._storage = isolateserver.get_storage(isolate_server, namespace)
454 else:
455 # Shards must all use exact same isolate server and namespace.
456 if self._storage.location != isolate_server:
457 logging.error(
458 'Task shards are using multiple isolate servers: %s and %s',
459 self._storage.location, isolate_server)
460 return None
461 if self._storage.namespace != namespace:
462 logging.error(
463 'Task shards are using multiple namespaces: %s and %s',
464 self._storage.namespace, namespace)
465 return None
466 return self._storage
467
468
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500469def now():
470 """Exists so it can be mocked easily."""
471 return time.time()
472
473
maruel77f720b2015-09-15 12:35:22 -0700474def parse_time(value):
475 """Converts serialized time from the API to datetime.datetime."""
476 # When microseconds are 0, the '.123456' suffix is elided. This means the
477 # serialized format is not consistent, which confuses the hell out of python.
478 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
479 try:
480 return datetime.datetime.strptime(value, fmt)
481 except ValueError:
482 pass
483 raise ValueError('Failed to parse %s' % value)
484
485
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700486def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700487 base_url, shard_index, task_id, timeout, should_stop, output_collector,
488 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400489 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700490
Vadim Shtayurab450c602014-05-12 19:23:25 -0700491 Returns:
492 <result dict> on success.
493 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494 """
maruel71c61c82016-02-22 06:52:05 -0800495 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700496 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700497 if include_perf:
498 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700499 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700500 started = now()
501 deadline = started + timeout if timeout else None
502 attempt = 0
503
504 while not should_stop.is_set():
505 attempt += 1
506
507 # Waiting for too long -> give up.
508 current_time = now()
509 if deadline and current_time >= deadline:
510 logging.error('retrieve_results(%s) timed out on attempt %d',
511 base_url, attempt)
512 return None
513
514 # Do not spin too fast. Spin faster at the beginning though.
515 # Start with 1 sec delay and for each 30 sec of waiting add another second
516 # of delay, until hitting 15 sec ceiling.
517 if attempt > 1:
518 max_delay = min(15, 1 + (current_time - started) / 30.0)
519 delay = min(max_delay, deadline - current_time) if deadline else max_delay
520 if delay > 0:
521 logging.debug('Waiting %.1f sec before retrying', delay)
522 should_stop.wait(delay)
523 if should_stop.is_set():
524 return None
525
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400526 # Disable internal retries in net.url_read_json, since we are doing retries
527 # ourselves.
528 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700529 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
530 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 result = net.url_read_json(result_url, retry_50x=False)
532 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400533 continue
maruel77f720b2015-09-15 12:35:22 -0700534
maruelbf53e042015-12-01 15:00:51 -0800535 if result.get('error'):
536 # An error occurred.
537 if result['error'].get('errors'):
538 for err in result['error']['errors']:
539 logging.warning(
540 'Error while reading task: %s; %s',
541 err.get('message'), err.get('debugInfo'))
542 elif result['error'].get('message'):
543 logging.warning(
544 'Error while reading task: %s', result['error']['message'])
545 continue
546
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400547 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700548 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400549 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700550 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700551 # Record the result, try to fetch attached output files (if any).
552 if output_collector:
553 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700554 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700555 if result.get('internal_failure'):
556 logging.error('Internal error!')
557 elif result['state'] == 'BOT_DIED':
558 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700559 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000560
561
maruel77f720b2015-09-15 12:35:22 -0700562def convert_to_old_format(result):
563 """Converts the task result data from Endpoints API format to old API format
564 for compatibility.
565
566 This goes into the file generated as --task-summary-json.
567 """
568 # Sets default.
569 result.setdefault('abandoned_ts', None)
570 result.setdefault('bot_id', None)
571 result.setdefault('bot_version', None)
572 result.setdefault('children_task_ids', [])
573 result.setdefault('completed_ts', None)
574 result.setdefault('cost_saved_usd', None)
575 result.setdefault('costs_usd', None)
576 result.setdefault('deduped_from', None)
577 result.setdefault('name', None)
578 result.setdefault('outputs_ref', None)
579 result.setdefault('properties_hash', None)
580 result.setdefault('server_versions', None)
581 result.setdefault('started_ts', None)
582 result.setdefault('tags', None)
583 result.setdefault('user', None)
584
585 # Convertion back to old API.
586 duration = result.pop('duration', None)
587 result['durations'] = [duration] if duration else []
588 exit_code = result.pop('exit_code', None)
589 result['exit_codes'] = [int(exit_code)] if exit_code else []
590 result['id'] = result.pop('task_id')
591 result['isolated_out'] = result.get('outputs_ref', None)
592 output = result.pop('output', None)
593 result['outputs'] = [output] if output else []
594 # properties_hash
595 # server_version
596 # Endpoints result 'state' as string. For compatibility with old code, convert
597 # to int.
598 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700599 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700600 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700601 if 'bot_dimensions' in result:
602 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700603 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700604 }
605 else:
606 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700607
608
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700609def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400610 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700611 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500612 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000613
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700614 Duplicate shards are ignored. Shards are yielded in order of completion.
615 Timed out shards are NOT yielded at all. Caller can compare number of yielded
616 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617
618 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500619 done. Since in general the number of task_keys is in the range <=10, it's not
maruel@chromium.org0437a732013-08-27 16:05:52 +0000620 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500621
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700622 output_collector is an optional instance of TaskOutputCollector that will be
623 used to fetch files produced by a task from isolate server to the local disk.
624
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500625 Yields:
626 (index, result). In particular, 'result' is defined as the
627 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000629 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400630 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700631 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700632 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700633
maruel@chromium.org0437a732013-08-27 16:05:52 +0000634 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
635 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700636 # Adds a task to the thread pool to call 'retrieve_results' and return
637 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400638 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700642 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700643
644 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400645 for shard_index, task_id in enumerate(task_ids):
646 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700647
648 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400649 shards_remaining = range(len(task_ids))
650 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700651 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700653 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654 shard_index, result = results_channel.pull(
655 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700656 except threading_utils.TaskChannel.Timeout:
657 if print_status_updates:
658 print(
659 'Waiting for results from the following shards: %s' %
660 ', '.join(map(str, shards_remaining)))
661 sys.stdout.flush()
662 continue
663 except Exception:
664 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700665
666 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700667 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000668 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500669 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000670 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700671
Vadim Shtayurab450c602014-05-12 19:23:25 -0700672 # Yield back results to the caller.
673 assert shard_index in shards_remaining
674 shards_remaining.remove(shard_index)
675 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700676
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 should_stop.set()
680
681
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400682def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000683 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700684 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400685 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700686 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
687 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400688 else:
689 pending = 'N/A'
690
maruel77f720b2015-09-15 12:35:22 -0700691 if metadata.get('duration') is not None:
692 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400693 else:
694 duration = 'N/A'
695
maruel77f720b2015-09-15 12:35:22 -0700696 if metadata.get('exit_code') is not None:
697 # Integers are encoded as string to not loose precision.
698 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400699 else:
700 exit_code = 'N/A'
701
702 bot_id = metadata.get('bot_id') or 'N/A'
703
maruel77f720b2015-09-15 12:35:22 -0700704 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400705 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400706 tag_footer = (
707 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
708 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400709
710 tag_len = max(len(tag_header), len(tag_footer))
711 dash_pad = '+-%s-+\n' % ('-' * tag_len)
712 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
713 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
714
715 header = dash_pad + tag_header + dash_pad
716 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700717 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400718 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000719
720
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700721def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700722 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700723 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700724 """Retrieves results of a Swarming task.
725
726 Returns:
727 process exit code that should be returned to the user.
728 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700729 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700730 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700732 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700733 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400734 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700735 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400736 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400737 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700738 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700739 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700740
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400741 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700742 shard_exit_code = metadata.get('exit_code')
743 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700744 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700745 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700746 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400747 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700748 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700749
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700750 if decorate:
leileied181762016-10-13 14:24:59 -0700751 s = decorate_shard_output(swarming, index, metadata).encode(
752 'utf-8', 'replace')
753 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400754 if len(seen_shards) < len(task_ids):
755 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700756 else:
maruel77f720b2015-09-15 12:35:22 -0700757 print('%s: %s %s' % (
758 metadata.get('bot_id', 'N/A'),
759 metadata['task_id'],
760 shard_exit_code))
761 if metadata['output']:
762 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400763 if output:
764 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700765 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700766 summary = output_collector.finalize()
767 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700768 # TODO(maruel): Make this optional.
769 for i in summary['shards']:
770 if i:
771 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700772 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400774 if decorate and total_duration:
775 print('Total duration: %.1fs' % total_duration)
776
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400777 if len(seen_shards) != len(task_ids):
778 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700779 print >> sys.stderr, ('Results from some shards are missing: %s' %
780 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700781 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782
maruela5490782015-09-30 10:56:59 -0700783 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000784
785
maruel77f720b2015-09-15 12:35:22 -0700786### API management.
787
788
789class APIError(Exception):
790 pass
791
792
793def endpoints_api_discovery_apis(host):
794 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
795 the APIs exposed by a host.
796
797 https://developers.google.com/discovery/v1/reference/apis/list
798 """
maruel380e3262016-08-31 16:10:06 -0700799 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
800 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700801 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
802 if data is None:
803 raise APIError('Failed to discover APIs on %s' % host)
804 out = {}
805 for api in data['items']:
806 if api['id'] == 'discovery:v1':
807 continue
808 # URL is of the following form:
809 # url = host + (
810 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
811 api_data = net.url_read_json(api['discoveryRestUrl'])
812 if api_data is None:
813 raise APIError('Failed to discover %s on %s' % (api['id'], host))
814 out[api['id']] = api_data
815 return out
816
817
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500818### Commands.
819
820
821def abort_task(_swarming, _manifest):
822 """Given a task manifest that was triggered, aborts its execution."""
823 # TODO(vadimsh): No supported by the server yet.
824
825
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400826def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800827 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500828 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500829 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500830 dest='dimensions', metavar='FOO bar',
831 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500832 parser.add_option_group(parser.filter_group)
833
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400834
Vadim Shtayurab450c602014-05-12 19:23:25 -0700835def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400836 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700837 parser.sharding_group.add_option(
838 '--shards', type='int', default=1,
839 help='Number of shards to trigger and collect.')
840 parser.add_option_group(parser.sharding_group)
841
842
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400843def add_trigger_options(parser):
844 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500845 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400846 add_filter_options(parser)
847
maruel681d6802017-01-17 16:56:03 -0800848 group = optparse.OptionGroup(parser, 'Task properties')
849 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500850 '-s', '--isolated',
851 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800852 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500853 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700854 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800855 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400856 '--idempotent', action='store_true', default=False,
857 help='When set, the server will actively try to find a previous task '
858 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800859 group.add_option(
iannuccieee1bca2016-10-28 13:16:23 -0700860 '--secret-bytes-path',
iannuccidc80dfb2016-10-28 12:50:20 -0700861 help='The optional path to a file containing the secret_bytes to use with'
862 'this task.')
maruel681d6802017-01-17 16:56:03 -0800863 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400864 '--hard-timeout', type='int', default=60*60,
865 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800866 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400867 '--io-timeout', type='int', default=20*60,
868 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800869 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500870 '--raw-cmd', action='store_true', default=False,
871 help='When set, the command after -- is used as-is without run_isolated. '
maruela9fe2cb2017-05-10 10:43:23 -0700872 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800873 group.add_option(
borenet02f772b2016-06-22 12:42:19 -0700874 '--cipd-package', action='append', default=[],
875 help='CIPD packages to install on the Swarming bot. Uses the format: '
876 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800877 group.add_option(
878 '--named-cache', action='append', nargs=2, default=[],
879 help='"<name> <relpath>" items to keep a persistent bot managed cache')
880 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700881 '--service-account',
882 help='Name of a service account to run the task as. Only literal "bot" '
883 'string can be specified currently (to run the task under bot\'s '
884 'account). Don\'t use task service accounts if not given '
885 '(default).')
maruel681d6802017-01-17 16:56:03 -0800886 group.add_option(
aludwincc5524e2016-10-28 10:25:24 -0700887 '-o', '--output', action='append', default=[],
888 help='A list of files to return in addition to those written to'
889 '$(ISOLATED_OUTDIR). An error will occur if a file specified by'
890 'this option is also written directly to $(ISOLATED_OUTDIR).')
maruel681d6802017-01-17 16:56:03 -0800891 parser.add_option_group(group)
892
893 group = optparse.OptionGroup(parser, 'Task request')
894 group.add_option(
895 '--priority', type='int', default=100,
896 help='The lower value, the more important the task is')
897 group.add_option(
898 '-T', '--task-name',
899 help='Display name of the task. Defaults to '
900 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
901 'isolated file is provided, if a hash is provided, it defaults to '
902 '<user>/<dimensions>/<isolated hash>/<timestamp>')
903 group.add_option(
904 '--tags', action='append', default=[],
905 help='Tags to assign to the task.')
906 group.add_option(
907 '--user', default='',
908 help='User associated with the task. Defaults to authenticated user on '
909 'the server.')
910 group.add_option(
911 '--expiration', type='int', default=6*60*60,
912 help='Seconds to allow the task to be pending for a bot to run before '
913 'this task request expires.')
914 group.add_option(
915 '--deadline', type='int', dest='expiration',
916 help=optparse.SUPPRESS_HELP)
917 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000918
919
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500920def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700921 """Processes trigger options and does preparatory steps.
922
maruel4e901792017-05-09 12:07:02 -0700923 Generates service account tokens if necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500924 """
925 options.dimensions = dict(options.dimensions)
926 options.env = dict(options.env)
maruela9fe2cb2017-05-10 10:43:23 -0700927 if args and args[0] == '--':
928 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500929
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500930 if not options.dimensions:
931 parser.error('Please at least specify one --dimension')
maruela9fe2cb2017-05-10 10:43:23 -0700932 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
933 parser.error('--tags must be in the format key:value')
934 if options.raw_cmd and not args:
935 parser.error(
936 'Arguments with --raw-cmd should be passed after -- as command '
937 'delimiter.')
938 if options.isolate_server and not options.namespace:
939 parser.error(
940 '--namespace must be a valid value when --isolate-server is used')
941 if not options.isolated and not options.raw_cmd:
942 parser.error('Specify at least one of --raw-cmd or --isolated or both')
943
944 # Isolated
945 # --isolated is required only if --raw-cmd wasn't provided.
946 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
947 # preferred server.
948 isolateserver.process_isolate_server_options(
949 parser, options, False, not options.raw_cmd)
950 inputs_ref = None
951 if options.isolate_server:
952 inputs_ref = FilesRef(
953 isolated=options.isolated,
954 isolatedserver=options.isolate_server,
955 namespace=options.namespace)
956
957 # Command
958 command = None
959 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500960 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500961 command = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500962 else:
maruela9fe2cb2017-05-10 10:43:23 -0700963 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500964
maruela9fe2cb2017-05-10 10:43:23 -0700965 # CIPD
borenet02f772b2016-06-22 12:42:19 -0700966 cipd_packages = []
967 for p in options.cipd_package:
968 split = p.split(':', 2)
969 if len(split) != 3:
970 parser.error('CIPD packages must take the form: path:package:version')
971 cipd_packages.append(CipdPackage(
972 package_name=split[1],
973 path=split[0],
974 version=split[2]))
975 cipd_input = None
976 if cipd_packages:
977 cipd_input = CipdInput(
978 client_package=None,
979 packages=cipd_packages,
980 server=None)
981
maruela9fe2cb2017-05-10 10:43:23 -0700982 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -0700983 secret_bytes = None
984 if options.secret_bytes_path:
985 with open(options.secret_bytes_path, 'r') as f:
986 secret_bytes = f.read().encode('base64')
987
maruela9fe2cb2017-05-10 10:43:23 -0700988 # Named caches
maruel681d6802017-01-17 16:56:03 -0800989 caches = [
990 {u'name': unicode(i[0]), u'path': unicode(i[1])}
991 for i in options.named_cache
992 ]
maruela9fe2cb2017-05-10 10:43:23 -0700993
maruel77f720b2015-09-15 12:35:22 -0700994 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -0800995 caches=caches,
borenet02f772b2016-06-22 12:42:19 -0700996 cipd_input=cipd_input,
maruela9fe2cb2017-05-10 10:43:23 -0700997 command=command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500998 dimensions=options.dimensions,
999 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001000 execution_timeout_secs=options.hard_timeout,
maruela9fe2cb2017-05-10 10:43:23 -07001001 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001002 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001003 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001004 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001005 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001006 outputs=options.output,
1007 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001008
1009 # Convert a service account email to a signed service account token to pass
1010 # to Swarming.
1011 service_account_token = None
1012 if options.service_account in ('bot', 'none'):
1013 service_account_token = options.service_account
1014 elif options.service_account:
1015 # pylint: disable=assignment-from-no-return
1016 service_account_token = mint_service_account_token(options.service_account)
1017
maruel77f720b2015-09-15 12:35:22 -07001018 return NewTaskRequest(
1019 expiration_secs=options.expiration,
maruela9fe2cb2017-05-10 10:43:23 -07001020 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001021 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001022 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001023 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001024 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001025 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001026 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001027
1028
1029def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001030 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001031 '-t', '--timeout', type='float',
1032 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1033 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001034 parser.group_logging.add_option(
1035 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001036 parser.group_logging.add_option(
1037 '--print-status-updates', action='store_true',
1038 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001039 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001040 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001041 '--task-summary-json',
1042 metavar='FILE',
1043 help='Dump a summary of task results to this file as json. It contains '
1044 'only shards statuses as know to server directly. Any output files '
1045 'emitted by the task can be collected by using --task-output-dir')
1046 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001047 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001048 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001049 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001050 'directory contains per-shard directory with output files produced '
1051 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001052 parser.task_output_group.add_option(
1053 '--perf', action='store_true', default=False,
1054 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001055 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001056
1057
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001058@subcommand.usage('bots...')
1059def CMDbot_delete(parser, args):
1060 """Forcibly deletes bots from the Swarming server."""
1061 parser.add_option(
1062 '-f', '--force', action='store_true',
1063 help='Do not prompt for confirmation')
1064 options, args = parser.parse_args(args)
1065 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001066 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001067
1068 bots = sorted(args)
1069 if not options.force:
1070 print('Delete the following bots?')
1071 for bot in bots:
1072 print(' %s' % bot)
1073 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1074 print('Goodbye.')
1075 return 1
1076
1077 result = 0
1078 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001079 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001080 if net.url_read_json(url, data={}, method='POST') is None:
1081 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001082 result = 1
1083 return result
1084
1085
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001086def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001087 """Returns information about the bots connected to the Swarming server."""
1088 add_filter_options(parser)
1089 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001090 '--dead-only', action='store_true',
1091 help='Only print dead bots, useful to reap them and reimage broken bots')
1092 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001093 '-k', '--keep-dead', action='store_true',
1094 help='Do not filter out dead bots')
1095 parser.filter_group.add_option(
1096 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001097 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001098 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001099
1100 if options.keep_dead and options.dead_only:
1101 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001102
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001103 bots = []
1104 cursor = None
1105 limit = 250
1106 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001107 base_url = (
maruel380e3262016-08-31 16:10:06 -07001108 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001109 while True:
1110 url = base_url
1111 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001112 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001113 data = net.url_read_json(url)
1114 if data is None:
1115 print >> sys.stderr, 'Failed to access %s' % options.swarming
1116 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001117 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001118 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001119 if not cursor:
1120 break
1121
maruel77f720b2015-09-15 12:35:22 -07001122 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001123 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001124 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001125 continue
maruel77f720b2015-09-15 12:35:22 -07001126 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001127 continue
1128
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001129 # If the user requested to filter on dimensions, ensure the bot has all the
1130 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001131 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001132 for key, value in options.dimensions:
1133 if key not in dimensions:
1134 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001135 # A bot can have multiple value for a key, for example,
1136 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1137 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001138 if isinstance(dimensions[key], list):
1139 if value not in dimensions[key]:
1140 break
1141 else:
1142 if value != dimensions[key]:
1143 break
1144 else:
maruel77f720b2015-09-15 12:35:22 -07001145 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001146 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001147 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001148 if bot.get('task_id'):
1149 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001150 return 0
1151
1152
maruelfd0a90c2016-06-10 11:51:10 -07001153@subcommand.usage('task_id')
1154def CMDcancel(parser, args):
1155 """Cancels a task."""
1156 options, args = parser.parse_args(args)
1157 if not args:
1158 parser.error('Please specify the task to cancel')
1159 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001160 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001161 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1162 print('Deleting %s failed. Probably already gone' % task_id)
1163 return 1
1164 return 0
1165
1166
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001167@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001168def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001169 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001170
1171 The result can be in multiple part if the execution was sharded. It can
1172 potentially have retries.
1173 """
1174 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001175 parser.add_option(
1176 '-j', '--json',
1177 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001178 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001179 if not args and not options.json:
1180 parser.error('Must specify at least one task id or --json.')
1181 if args and options.json:
1182 parser.error('Only use one of task id or --json.')
1183
1184 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001185 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001186 try:
maruel1ceb3872015-10-14 06:10:44 -07001187 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001188 data = json.load(f)
1189 except (IOError, ValueError):
1190 parser.error('Failed to open %s' % options.json)
1191 try:
1192 tasks = sorted(
1193 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1194 args = [t['task_id'] for t in tasks]
1195 except (KeyError, TypeError):
1196 parser.error('Failed to process %s' % options.json)
1197 if options.timeout is None:
1198 options.timeout = (
1199 data['request']['properties']['execution_timeout_secs'] +
1200 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001201 else:
1202 valid = frozenset('0123456789abcdef')
1203 if any(not valid.issuperset(task_id) for task_id in args):
1204 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001205
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001206 try:
1207 return collect(
1208 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001209 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001210 options.timeout,
1211 options.decorate,
1212 options.print_status_updates,
1213 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001214 options.task_output_dir,
1215 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001216 except Failure:
1217 on_error.report(None)
1218 return 1
1219
1220
maruelbea00862015-09-18 09:55:36 -07001221@subcommand.usage('[filename]')
1222def CMDput_bootstrap(parser, args):
1223 """Uploads a new version of bootstrap.py."""
1224 options, args = parser.parse_args(args)
1225 if len(args) != 1:
1226 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001227 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001228 path = unicode(os.path.abspath(args[0]))
1229 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001230 content = f.read().decode('utf-8')
1231 data = net.url_read_json(url, data={'content': content})
1232 print data
1233 return 0
1234
1235
1236@subcommand.usage('[filename]')
1237def CMDput_bot_config(parser, args):
1238 """Uploads a new version of bot_config.py."""
1239 options, args = parser.parse_args(args)
1240 if len(args) != 1:
1241 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001242 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001243 path = unicode(os.path.abspath(args[0]))
1244 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001245 content = f.read().decode('utf-8')
1246 data = net.url_read_json(url, data={'content': content})
1247 print data
1248 return 0
1249
1250
maruel77f720b2015-09-15 12:35:22 -07001251@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001252def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001253 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1254 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001255
1256 Examples:
maruel77f720b2015-09-15 12:35:22 -07001257 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001258 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001259
maruel77f720b2015-09-15 12:35:22 -07001260 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001261 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1262
1263 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1264 quoting is important!:
1265 swarming.py query -S server-url.com --limit 10 \\
1266 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001267 """
1268 CHUNK_SIZE = 250
1269
1270 parser.add_option(
1271 '-L', '--limit', type='int', default=200,
1272 help='Limit to enforce on limitless items (like number of tasks); '
1273 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001274 parser.add_option(
1275 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001276 parser.add_option(
1277 '--progress', action='store_true',
1278 help='Prints a dot at each request to show progress')
1279 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001280 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001281 parser.error(
1282 'Must specify only method name and optionally query args properly '
1283 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001284 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001285 url = base_url
1286 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001287 # Check check, change if not working out.
1288 merge_char = '&' if '?' in url else '?'
1289 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001290 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001291 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001292 # TODO(maruel): Do basic diagnostic.
1293 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001294 return 1
1295
1296 # Some items support cursors. Try to get automatically if cursors are needed
1297 # by looking at the 'cursor' items.
1298 while (
1299 data.get('cursor') and
1300 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001301 merge_char = '&' if '?' in base_url else '?'
1302 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001303 if options.limit:
1304 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001305 if options.progress:
1306 sys.stdout.write('.')
1307 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001308 new = net.url_read_json(url)
1309 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001310 if options.progress:
1311 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001312 print >> sys.stderr, 'Failed to access %s' % options.swarming
1313 return 1
maruel81b37132015-10-21 06:42:13 -07001314 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001315 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001316
maruel77f720b2015-09-15 12:35:22 -07001317 if options.progress:
1318 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001319 if options.limit and len(data.get('items', [])) > options.limit:
1320 data['items'] = data['items'][:options.limit]
1321 data.pop('cursor', None)
1322
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001323 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001324 options.json = unicode(os.path.abspath(options.json))
1325 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001326 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001327 try:
maruel77f720b2015-09-15 12:35:22 -07001328 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001329 sys.stdout.write('\n')
1330 except IOError:
1331 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001332 return 0
1333
1334
maruel77f720b2015-09-15 12:35:22 -07001335def CMDquery_list(parser, args):
1336 """Returns list of all the Swarming APIs that can be used with command
1337 'query'.
1338 """
1339 parser.add_option(
1340 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1341 options, args = parser.parse_args(args)
1342 if args:
1343 parser.error('No argument allowed.')
1344
1345 try:
1346 apis = endpoints_api_discovery_apis(options.swarming)
1347 except APIError as e:
1348 parser.error(str(e))
1349 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001350 options.json = unicode(os.path.abspath(options.json))
1351 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001352 json.dump(apis, f)
1353 else:
1354 help_url = (
1355 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1356 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001357 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1358 if i:
1359 print('')
maruel77f720b2015-09-15 12:35:22 -07001360 print api_id
maruel11e31af2017-02-15 07:30:50 -08001361 print ' ' + api['description'].strip()
1362 if 'resources' in api:
1363 # Old.
1364 for j, (resource_name, resource) in enumerate(
1365 sorted(api['resources'].iteritems())):
1366 if j:
1367 print('')
1368 for method_name, method in sorted(resource['methods'].iteritems()):
1369 # Only list the GET ones.
1370 if method['httpMethod'] != 'GET':
1371 continue
1372 print '- %s.%s: %s' % (
1373 resource_name, method_name, method['path'])
1374 print('\n'.join(
1375 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1376 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1377 else:
1378 # New.
1379 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001380 # Only list the GET ones.
1381 if method['httpMethod'] != 'GET':
1382 continue
maruel11e31af2017-02-15 07:30:50 -08001383 print '- %s: %s' % (method['id'], method['path'])
1384 print('\n'.join(
1385 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001386 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1387 return 0
1388
1389
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001390@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001391def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001392 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001393
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001394 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001395 """
1396 add_trigger_options(parser)
1397 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001398 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001399 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001400 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001401 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001402 tasks = trigger_task_shards(
1403 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001404 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001405 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001406 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001407 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001408 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001409 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001410 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001411 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001412 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001413 task_ids = [
1414 t['task_id']
1415 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1416 ]
maruel71c61c82016-02-22 06:52:05 -08001417 if options.timeout is None:
1418 options.timeout = (
1419 task_request.properties.execution_timeout_secs +
1420 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001421 try:
1422 return collect(
1423 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001424 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001425 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001426 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001427 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001428 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001429 options.task_output_dir,
1430 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001431 except Failure:
1432 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001433 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001434
1435
maruel18122c62015-10-23 06:31:23 -07001436@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001437def CMDreproduce(parser, args):
1438 """Runs a task locally that was triggered on the server.
1439
1440 This running locally the same commands that have been run on the bot. The data
1441 downloaded will be in a subdirectory named 'work' of the current working
1442 directory.
maruel18122c62015-10-23 06:31:23 -07001443
1444 You can pass further additional arguments to the target command by passing
1445 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001446 """
maruelc070e672016-02-22 17:32:57 -08001447 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001448 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001449 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001450 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001451 extra_args = []
1452 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001453 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001454 if len(args) > 1:
1455 if args[1] == '--':
1456 if len(args) > 2:
1457 extra_args = args[2:]
1458 else:
1459 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001460
maruel380e3262016-08-31 16:10:06 -07001461 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001462 request = net.url_read_json(url)
1463 if not request:
1464 print >> sys.stderr, 'Failed to retrieve request data for the task'
1465 return 1
1466
maruel12e30012015-10-09 11:55:35 -07001467 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001468 if fs.isdir(workdir):
1469 parser.error('Please delete the directory \'work\' first')
1470 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001471 cachedir = unicode(os.path.abspath('cipd_cache'))
1472 if not fs.exists(cachedir):
1473 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001474
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001475 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001476 env = os.environ.copy()
1477 env['SWARMING_BOT_ID'] = 'reproduce'
1478 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001479 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001480 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001481 for i in properties['env']:
1482 key = i['key'].encode('utf-8')
1483 if not i['value']:
1484 env.pop(key, None)
1485 else:
1486 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001487
iannucci31ab9192017-05-02 19:11:56 -07001488 command = []
nodir152cba62016-05-12 16:08:56 -07001489 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001490 # Create the tree.
1491 with isolateserver.get_storage(
1492 properties['inputs_ref']['isolatedserver'],
1493 properties['inputs_ref']['namespace']) as storage:
1494 bundle = isolateserver.fetch_isolated(
1495 properties['inputs_ref']['isolated'],
1496 storage,
1497 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001498 workdir,
1499 False)
maruel29ab2fd2015-10-16 11:44:01 -07001500 command = bundle.command
1501 if bundle.relative_cwd:
1502 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001503 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001504
1505 if properties.get('command'):
1506 command.extend(properties['command'])
1507
1508 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1509 new_command = tools.fix_python_path(command)
1510 new_command = run_isolated.process_command(
1511 new_command, options.output_dir, None)
1512 if not options.output_dir and new_command != command:
1513 parser.error('The task has outputs, you must use --output-dir')
1514 command = new_command
1515 file_path.ensure_command_has_abs_path(command, workdir)
1516
1517 if properties.get('cipd_input'):
1518 ci = properties['cipd_input']
1519 cp = ci['client_package']
1520 client_manager = cipd.get_client(
1521 ci['server'], cp['package_name'], cp['version'], cachedir)
1522
1523 with client_manager as client:
1524 by_path = collections.defaultdict(list)
1525 for pkg in ci['packages']:
1526 path = pkg['path']
1527 # cipd deals with 'root' as ''
1528 if path == '.':
1529 path = ''
1530 by_path[path].append((pkg['package_name'], pkg['version']))
1531 client.ensure(workdir, by_path, cache_dir=cachedir)
1532
maruel77f720b2015-09-15 12:35:22 -07001533 try:
maruel18122c62015-10-23 06:31:23 -07001534 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001535 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001536 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001537 print >> sys.stderr, str(e)
1538 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001539
1540
maruel0eb1d1b2015-10-02 14:48:21 -07001541@subcommand.usage('bot_id')
1542def CMDterminate(parser, args):
1543 """Tells a bot to gracefully shut itself down as soon as it can.
1544
1545 This is done by completing whatever current task there is then exiting the bot
1546 process.
1547 """
1548 parser.add_option(
1549 '--wait', action='store_true', help='Wait for the bot to terminate')
1550 options, args = parser.parse_args(args)
1551 if len(args) != 1:
1552 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001553 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001554 request = net.url_read_json(url, data={})
1555 if not request:
1556 print >> sys.stderr, 'Failed to ask for termination'
1557 return 1
1558 if options.wait:
1559 return collect(
maruel9531ce02016-04-13 06:11:23 -07001560 options.swarming, [request['task_id']], 0., False, False, None, None,
1561 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001562 return 0
1563
1564
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001565@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001566def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001567 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001568
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001569 Passes all extra arguments provided after '--' as additional command line
1570 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001571 """
1572 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001573 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001574 parser.add_option(
1575 '--dump-json',
1576 metavar='FILE',
1577 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001578 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001579 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001580 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001581 tasks = trigger_task_shards(
1582 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001583 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001584 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001585 tasks_sorted = sorted(
1586 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001587 if options.dump_json:
1588 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001589 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001590 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001591 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001592 }
maruel46b015f2015-10-13 18:40:35 -07001593 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001594 print('To collect results, use:')
1595 print(' swarming.py collect -S %s --json %s' %
1596 (options.swarming, options.dump_json))
1597 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001598 print('To collect results, use:')
1599 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001600 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1601 print('Or visit:')
1602 for t in tasks_sorted:
1603 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001604 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001605 except Failure:
1606 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001607 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001608
1609
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001610class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001611 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001612 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001613 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001614 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001615 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001616 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001617 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001618 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001619 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001620 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001621
1622 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001623 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001624 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001625 auth.process_auth_options(self, options)
1626 user = self._process_swarming(options)
1627 if hasattr(options, 'user') and not options.user:
1628 options.user = user
1629 return options, args
1630
1631 def _process_swarming(self, options):
1632 """Processes the --swarming option and aborts if not specified.
1633
1634 Returns the identity as determined by the server.
1635 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001636 if not options.swarming:
1637 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001638 try:
1639 options.swarming = net.fix_url(options.swarming)
1640 except ValueError as e:
1641 self.error('--swarming %s' % e)
1642 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001643 try:
1644 user = auth.ensure_logged_in(options.swarming)
1645 except ValueError as e:
1646 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001647 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001648
1649
1650def main(args):
1651 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001652 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001653
1654
1655if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001656 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001657 fix_encoding.fix_encoding()
1658 tools.disable_buffering()
1659 colorama.init()
1660 sys.exit(main(sys.argv[1:]))