blob: db551902434163a846fade3988bf77c602ac2eaa [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
Robert Iannuccifafa7352018-06-13 17:08:17 +00008__version__ = '0.13'
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
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import 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
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +000040import isolate_storage
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +000041import isolated_format
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040042import local_caching
maruelc070e672016-02-22 17:32:57 -080043import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000044
45
tansella4949442016-06-23 22:34:32 -070046ROOT_DIR = os.path.dirname(os.path.abspath(
47 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050048
49
50class Failure(Exception):
51 """Generic failure."""
52 pass
53
54
maruel0a25f6c2017-05-10 10:43:23 -070055def default_task_name(options):
56 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050057 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070058 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070059 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070060 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070061 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.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800101StringListPair = collections.namedtuple(
102 'StringListPair', [
103 'key',
104 'value', # repeated string
105 ]
106)
107
108
109# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700110TaskProperties = collections.namedtuple(
111 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500112 [
maruel681d6802017-01-17 16:56:03 -0800113 'caches',
borenet02f772b2016-06-22 12:42:19 -0700114 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500115 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500116 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500117 'dimensions',
118 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800119 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700120 'execution_timeout_secs',
121 'extra_args',
122 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500123 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700124 'inputs_ref',
125 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700126 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700127 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700128 ])
129
130
131# See ../appengine/swarming/swarming_rpcs.py.
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400132TaskSlice = collections.namedtuple(
133 'TaskSlice',
134 [
135 'expiration_secs',
136 'properties',
137 'wait_for_capacity',
138 ])
139
140
141# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700142NewTaskRequest = collections.namedtuple(
143 'NewTaskRequest',
144 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500145 'name',
maruel77f720b2015-09-15 12:35:22 -0700146 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500147 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400148 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700149 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500150 'tags',
151 'user',
Robert Iannuccifafa7352018-06-13 17:08:17 +0000152 'pool_task_template',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500153 ])
154
155
maruel77f720b2015-09-15 12:35:22 -0700156def namedtuple_to_dict(value):
157 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400158 if hasattr(value, '_asdict'):
159 return namedtuple_to_dict(value._asdict())
160 if isinstance(value, (list, tuple)):
161 return [namedtuple_to_dict(v) for v in value]
162 if isinstance(value, dict):
163 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
164 return value
maruel77f720b2015-09-15 12:35:22 -0700165
166
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700167def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800168 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700169
170 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 """
maruel77f720b2015-09-15 12:35:22 -0700172 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700173 # Don't send 'service_account' if it is None to avoid confusing older
174 # version of the server that doesn't know about 'service_account' and don't
175 # use it at all.
176 if not out['service_account']:
177 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000178 for task_slice in out['task_slices']:
179 task_slice['properties']['env'] = [
180 {'key': k, 'value': v}
181 for k, v in task_slice['properties']['env'].iteritems()
182 ]
183 task_slice['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700184 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500185
186
maruel77f720b2015-09-15 12:35:22 -0700187def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500188 """Triggers a request on the Swarming server and returns the json data.
189
190 It's the low-level function.
191
192 Returns:
193 {
194 'request': {
195 'created_ts': u'2010-01-02 03:04:05',
196 'name': ..
197 },
198 'task_id': '12300',
199 }
200 """
201 logging.info('Triggering: %s', raw_request['name'])
202
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500203 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700204 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500205 if not result:
206 on_error.report('Failed to trigger task %s' % raw_request['name'])
207 return None
maruele557bce2015-11-17 09:01:27 -0800208 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800209 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800210 msg = 'Failed to trigger task %s' % raw_request['name']
211 if result['error'].get('errors'):
212 for err in result['error']['errors']:
213 if err.get('message'):
214 msg += '\nMessage: %s' % err['message']
215 if err.get('debugInfo'):
216 msg += '\nDebug info:\n%s' % err['debugInfo']
217 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800218 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800219
220 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800221 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500222 return result
223
224
225def setup_googletest(env, shards, index):
226 """Sets googletest specific environment variables."""
227 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700228 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
229 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
230 env = env[:]
231 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
232 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500233 return env
234
235
236def trigger_task_shards(swarming, task_request, shards):
237 """Triggers one or many subtasks of a sharded task.
238
239 Returns:
240 Dict with task details, returned to caller as part of --dump-json output.
241 None in case of failure.
242 """
243 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700244 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500245 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400246 req['task_slices'][0]['properties']['env'] = setup_googletest(
247 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700248 req['name'] += ':%s:%s' % (index, shards)
249 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250
251 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500252 tasks = {}
253 priority_warning = False
254 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700255 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500256 if not task:
257 break
258 logging.info('Request result: %s', task)
259 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400260 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500261 priority_warning = True
262 print >> sys.stderr, (
263 'Priority was reset to %s' % task['request']['priority'])
264 tasks[request['name']] = {
265 'shard_index': index,
266 'task_id': task['task_id'],
267 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
268 }
269
270 # Some shards weren't triggered. Abort everything.
271 if len(tasks) != len(requests):
272 if tasks:
273 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
274 len(tasks), len(requests))
275 for task_dict in tasks.itervalues():
276 abort_task(swarming, task_dict['task_id'])
277 return None
278
279 return tasks
280
281
282### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000283
284
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700285# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000286STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700287
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400288
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000289class TaskState(object):
290 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000291
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000292 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
293 is the source of truth for these values:
294 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400295
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000296 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400297 """
298 RUNNING = 0x10
299 PENDING = 0x20
300 EXPIRED = 0x30
301 TIMED_OUT = 0x40
302 BOT_DIED = 0x50
303 CANCELED = 0x60
304 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400305 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400306 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400307
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000308 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400309
maruel77f720b2015-09-15 12:35:22 -0700310 _ENUMS = {
311 'RUNNING': RUNNING,
312 'PENDING': PENDING,
313 'EXPIRED': EXPIRED,
314 'TIMED_OUT': TIMED_OUT,
315 'BOT_DIED': BOT_DIED,
316 'CANCELED': CANCELED,
317 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400318 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400319 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700320 }
321
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400322 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700323 def from_enum(cls, state):
324 """Returns int value based on the string."""
325 if state not in cls._ENUMS:
326 raise ValueError('Invalid state %s' % state)
327 return cls._ENUMS[state]
328
maruel@chromium.org0437a732013-08-27 16:05:52 +0000329
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700330class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700331 """Assembles task execution summary (for --task-summary-json output).
332
333 Optionally fetches task outputs from isolate server to local disk (used when
334 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700335
336 This object is shared among multiple threads running 'retrieve_results'
337 function, in particular they call 'process_shard_result' method in parallel.
338 """
339
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000340 def __init__(self, task_output_dir, task_output_stdout, shard_count,
341 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700342 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
343
344 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700345 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700346 shard_count: expected number of task shards.
347 """
maruel12e30012015-10-09 11:55:35 -0700348 self.task_output_dir = (
349 unicode(os.path.abspath(task_output_dir))
350 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000351 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700352 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000353 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700354
355 self._lock = threading.Lock()
356 self._per_shard_results = {}
357 self._storage = None
358
nodire5028a92016-04-29 14:38:21 -0700359 if self.task_output_dir:
360 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361
Vadim Shtayurab450c602014-05-12 19:23:25 -0700362 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 """Stores results of a single task shard, fetches output files if necessary.
364
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400365 Modifies |result| in place.
366
maruel77f720b2015-09-15 12:35:22 -0700367 shard_index is 0-based.
368
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369 Called concurrently from multiple threads.
370 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700371 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700372 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700373 if shard_index < 0 or shard_index >= self.shard_count:
374 logging.warning(
375 'Shard index %d is outside of expected range: [0; %d]',
376 shard_index, self.shard_count - 1)
377 return
378
maruel77f720b2015-09-15 12:35:22 -0700379 if result.get('outputs_ref'):
380 ref = result['outputs_ref']
381 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
382 ref['isolatedserver'],
383 urllib.urlencode(
384 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400385
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 # Store result dict of that shard, ignore results we've already seen.
387 with self._lock:
388 if shard_index in self._per_shard_results:
389 logging.warning('Ignoring duplicate shard index %d', shard_index)
390 return
391 self._per_shard_results[shard_index] = result
392
393 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700394 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000395 server_ref = isolate_storage.ServerRef(
396 result['outputs_ref']['isolatedserver'],
397 result['outputs_ref']['namespace'])
398 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400399 if storage:
400 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400401 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
402 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400403 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700404 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400405 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400406 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700407 os.path.join(self.task_output_dir, str(shard_index)),
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000408 False, self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700409
410 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700411 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700412 with self._lock:
413 # Write an array of shard results with None for missing shards.
414 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700415 'shards': [
416 self._per_shard_results.get(i) for i in xrange(self.shard_count)
417 ],
418 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000419
420 # Don't store stdout in the summary if not requested too.
421 if "json" not in self.task_output_stdout:
422 for shard_json in summary['shards']:
423 if not shard_json:
424 continue
425 if "output" in shard_json:
426 del shard_json["output"]
427 if "outputs" in shard_json:
428 del shard_json["outputs"]
429
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700430 # Write summary.json to task_output_dir as well.
431 if self.task_output_dir:
432 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700433 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700434 summary,
435 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700436 if self._storage:
437 self._storage.close()
438 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700439 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700440
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000441 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700443 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444 with self._lock:
445 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000446 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447 else:
448 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000449 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700450 logging.error(
451 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000452 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700453 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000454 if self._storage.server_ref.namespace != server_ref.namespace:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700455 logging.error(
456 'Task shards are using multiple namespaces: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000457 self._storage.server_ref.namespace, server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700458 return None
459 return self._storage
460
461
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500462def now():
463 """Exists so it can be mocked easily."""
464 return time.time()
465
466
maruel77f720b2015-09-15 12:35:22 -0700467def parse_time(value):
468 """Converts serialized time from the API to datetime.datetime."""
469 # When microseconds are 0, the '.123456' suffix is elided. This means the
470 # serialized format is not consistent, which confuses the hell out of python.
471 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
472 try:
473 return datetime.datetime.strptime(value, fmt)
474 except ValueError:
475 pass
476 raise ValueError('Failed to parse %s' % value)
477
478
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700479def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700480 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000481 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400482 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700483
Vadim Shtayurab450c602014-05-12 19:23:25 -0700484 Returns:
485 <result dict> on success.
486 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700487 """
maruel71c61c82016-02-22 06:52:05 -0800488 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700489 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700490 if include_perf:
491 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700492 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700493 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400494 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700495 attempt = 0
496
497 while not should_stop.is_set():
498 attempt += 1
499
500 # Waiting for too long -> give up.
501 current_time = now()
502 if deadline and current_time >= deadline:
503 logging.error('retrieve_results(%s) timed out on attempt %d',
504 base_url, attempt)
505 return None
506
507 # Do not spin too fast. Spin faster at the beginning though.
508 # Start with 1 sec delay and for each 30 sec of waiting add another second
509 # of delay, until hitting 15 sec ceiling.
510 if attempt > 1:
511 max_delay = min(15, 1 + (current_time - started) / 30.0)
512 delay = min(max_delay, deadline - current_time) if deadline else max_delay
513 if delay > 0:
514 logging.debug('Waiting %.1f sec before retrying', delay)
515 should_stop.wait(delay)
516 if should_stop.is_set():
517 return None
518
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400519 # Disable internal retries in net.url_read_json, since we are doing retries
520 # ourselves.
521 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700522 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
523 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400524 # Retry on 500s only if no timeout is specified.
525 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400526 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400527 if timeout == -1:
528 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400529 continue
maruel77f720b2015-09-15 12:35:22 -0700530
maruelbf53e042015-12-01 15:00:51 -0800531 if result.get('error'):
532 # An error occurred.
533 if result['error'].get('errors'):
534 for err in result['error']['errors']:
535 logging.warning(
536 'Error while reading task: %s; %s',
537 err.get('message'), err.get('debugInfo'))
538 elif result['error'].get('message'):
539 logging.warning(
540 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400541 if timeout == -1:
542 return result
maruelbf53e042015-12-01 15:00:51 -0800543 continue
544
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400545 # When timeout == -1, always return on first attempt. 500s are already
546 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000547 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000548 if fetch_stdout:
549 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700550 result['output'] = out.get('output', '') if out else ''
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)
maruel77f720b2015-09-15 12:35:22 -0700579 result.setdefault('server_versions', None)
580 result.setdefault('started_ts', None)
581 result.setdefault('tags', None)
582 result.setdefault('user', None)
583
584 # Convertion back to old API.
585 duration = result.pop('duration', None)
586 result['durations'] = [duration] if duration else []
587 exit_code = result.pop('exit_code', None)
588 result['exit_codes'] = [int(exit_code)] if exit_code else []
589 result['id'] = result.pop('task_id')
590 result['isolated_out'] = result.get('outputs_ref', None)
591 output = result.pop('output', None)
592 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700593 # server_version
594 # Endpoints result 'state' as string. For compatibility with old code, convert
595 # to int.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000596 result['state'] = TaskState.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700597 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700598 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700599 if 'bot_dimensions' in result:
600 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700601 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700602 }
603 else:
604 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700605
606
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700607def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400608 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000609 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500610 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000611
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700612 Duplicate shards are ignored. Shards are yielded in order of completion.
613 Timed out shards are NOT yielded at all. Caller can compare number of yielded
614 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000615
616 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500617 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 +0000618 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500619
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700620 output_collector is an optional instance of TaskOutputCollector that will be
621 used to fetch files produced by a task from isolate server to the local disk.
622
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500623 Yields:
624 (index, result). In particular, 'result' is defined as the
625 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000626 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000627 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400628 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700629 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700630 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700631
maruel@chromium.org0437a732013-08-27 16:05:52 +0000632 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
633 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634 # Adds a task to the thread pool to call 'retrieve_results' and return
635 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400636 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000637 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700638 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400640 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000641 task_id, timeout, should_stop, output_collector, include_perf,
642 fetch_stdout)
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:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000658 time_now = str(datetime.datetime.now())
659 _, time_now = time_now.split(' ')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 print(
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000661 '%s '
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700662 'Waiting for results from the following shards: %s' %
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000663 (time_now, ', '.join(map(str, shards_remaining)))
664 )
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700665 sys.stdout.flush()
666 continue
667 except Exception:
668 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700669
670 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700671 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000672 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500673 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000674 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700675
Vadim Shtayurab450c602014-05-12 19:23:25 -0700676 # Yield back results to the caller.
677 assert shard_index in shards_remaining
678 shards_remaining.remove(shard_index)
679 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700680
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700682 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000683 should_stop.set()
684
685
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000686def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000687 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700688 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400689 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700690 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
691 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400692 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
693 metadata.get('abandoned_ts')):
694 pending = '%.1fs' % (
695 parse_time(metadata['abandoned_ts']) -
696 parse_time(metadata['created_ts'])
697 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400698 else:
699 pending = 'N/A'
700
maruel77f720b2015-09-15 12:35:22 -0700701 if metadata.get('duration') is not None:
702 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400703 else:
704 duration = 'N/A'
705
maruel77f720b2015-09-15 12:35:22 -0700706 if metadata.get('exit_code') is not None:
707 # Integers are encoded as string to not loose precision.
708 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400709 else:
710 exit_code = 'N/A'
711
712 bot_id = metadata.get('bot_id') or 'N/A'
713
maruel77f720b2015-09-15 12:35:22 -0700714 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400715 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000716 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400717 if metadata.get('state') == 'CANCELED':
718 tag_footer2 = ' Pending: %s CANCELED' % pending
719 elif metadata.get('state') == 'EXPIRED':
720 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400721 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400722 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
723 pending, duration, bot_id, exit_code, metadata['state'])
724 else:
725 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
726 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400727
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000728 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
729 dash_pad = '+-%s-+' % ('-' * tag_len)
730 tag_header = '| %s |' % tag_header.ljust(tag_len)
731 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
732 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400733
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000734 if include_stdout:
735 return '\n'.join([
736 dash_pad,
737 tag_header,
738 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400739 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000740 dash_pad,
741 tag_footer1,
742 tag_footer2,
743 dash_pad,
744 ])
745 else:
746 return '\n'.join([
747 dash_pad,
748 tag_header,
749 tag_footer2,
750 dash_pad,
751 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000752
753
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700754def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700755 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000756 task_summary_json, task_output_dir, task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000757 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700758 """Retrieves results of a Swarming task.
759
760 Returns:
761 process exit code that should be returned to the user.
762 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000763
764 filter_cb = None
765 if filepath_filter:
766 filter_cb = re.compile(filepath_filter).match
767
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700768 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000769 output_collector = TaskOutputCollector(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000770 task_output_dir, task_output_stdout, len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700771
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700772 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700773 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400774 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700775 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400776 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400777 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000778 output_collector, include_perf,
779 (len(task_output_stdout) > 0),
780 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700781 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700782
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400783 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700784 shard_exit_code = metadata.get('exit_code')
785 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700786 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700787 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700788 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400789 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700790 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700791
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700792 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000793 s = decorate_shard_output(
794 swarming, index, metadata,
795 "console" in task_output_stdout).encode(
796 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700797 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400798 if len(seen_shards) < len(task_ids):
799 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700800 else:
maruel77f720b2015-09-15 12:35:22 -0700801 print('%s: %s %s' % (
802 metadata.get('bot_id', 'N/A'),
803 metadata['task_id'],
804 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000805 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700806 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400807 if output:
808 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700809 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700810 summary = output_collector.finalize()
811 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700812 # TODO(maruel): Make this optional.
813 for i in summary['shards']:
814 if i:
815 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700816 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700817
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400818 if decorate and total_duration:
819 print('Total duration: %.1fs' % total_duration)
820
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400821 if len(seen_shards) != len(task_ids):
822 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700823 print >> sys.stderr, ('Results from some shards are missing: %s' %
824 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700825 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700826
maruela5490782015-09-30 10:56:59 -0700827 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000828
829
maruel77f720b2015-09-15 12:35:22 -0700830### API management.
831
832
833class APIError(Exception):
834 pass
835
836
837def endpoints_api_discovery_apis(host):
838 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
839 the APIs exposed by a host.
840
841 https://developers.google.com/discovery/v1/reference/apis/list
842 """
maruel380e3262016-08-31 16:10:06 -0700843 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
844 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700845 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
846 if data is None:
847 raise APIError('Failed to discover APIs on %s' % host)
848 out = {}
849 for api in data['items']:
850 if api['id'] == 'discovery:v1':
851 continue
852 # URL is of the following form:
853 # url = host + (
854 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
855 api_data = net.url_read_json(api['discoveryRestUrl'])
856 if api_data is None:
857 raise APIError('Failed to discover %s on %s' % (api['id'], host))
858 out[api['id']] = api_data
859 return out
860
861
maruelaf6b06c2017-06-08 06:26:53 -0700862def get_yielder(base_url, limit):
863 """Returns the first query and a function that yields following items."""
864 CHUNK_SIZE = 250
865
866 url = base_url
867 if limit:
868 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
869 data = net.url_read_json(url)
870 if data is None:
871 # TODO(maruel): Do basic diagnostic.
872 raise Failure('Failed to access %s' % url)
873 org_cursor = data.pop('cursor', None)
874 org_total = len(data.get('items') or [])
875 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
876 if not org_cursor or not org_total:
877 # This is not an iterable resource.
878 return data, lambda: []
879
880 def yielder():
881 cursor = org_cursor
882 total = org_total
883 # Some items support cursors. Try to get automatically if cursors are needed
884 # by looking at the 'cursor' items.
885 while cursor and (not limit or total < limit):
886 merge_char = '&' if '?' in base_url else '?'
887 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
888 if limit:
889 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
890 new = net.url_read_json(url)
891 if new is None:
892 raise Failure('Failed to access %s' % url)
893 cursor = new.get('cursor')
894 new_items = new.get('items')
895 nb_items = len(new_items or [])
896 total += nb_items
897 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
898 yield new_items
899
900 return data, yielder
901
902
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500903### Commands.
904
905
906def abort_task(_swarming, _manifest):
907 """Given a task manifest that was triggered, aborts its execution."""
908 # TODO(vadimsh): No supported by the server yet.
909
910
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400911def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800912 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500913 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500914 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500915 dest='dimensions', metavar='FOO bar',
916 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000917 parser.filter_group.add_option(
918 '--optional-dimension', default=[], action='append', nargs=3,
919 dest='optional_dimensions', metavar='key value expiration',
920 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500921 parser.add_option_group(parser.filter_group)
922
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400923
Brad Hallf78187a2018-10-19 17:08:55 +0000924def _validate_filter_option(parser, key, value, expiration, argname):
925 if ':' in key:
926 parser.error('%s key cannot contain ":"' % argname)
927 if key.strip() != key:
928 parser.error('%s key has whitespace' % argname)
929 if not key:
930 parser.error('%s key is empty' % argname)
931
932 if value.strip() != value:
933 parser.error('%s value has whitespace' % argname)
934 if not value:
935 parser.error('%s value is empty' % argname)
936
937 if expiration is not None:
938 try:
939 expiration = int(expiration)
940 except ValueError:
941 parser.error('%s expiration is not an integer' % argname)
942 if expiration <= 0:
943 parser.error('%s expiration should be positive' % argname)
944 if expiration % 60 != 0:
945 parser.error('%s expiration is not divisible by 60' % argname)
946
947
maruelaf6b06c2017-06-08 06:26:53 -0700948def process_filter_options(parser, options):
949 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000950 _validate_filter_option(parser, key, value, None, 'dimension')
951 for key, value, exp in options.optional_dimensions:
952 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700953 options.dimensions.sort()
954
955
Vadim Shtayurab450c602014-05-12 19:23:25 -0700956def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400957 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700958 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700959 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700960 help='Number of shards to trigger and collect.')
961 parser.add_option_group(parser.sharding_group)
962
963
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400964def add_trigger_options(parser):
965 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500966 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400967 add_filter_options(parser)
968
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400969 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800970 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700971 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500972 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800973 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500974 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700975 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800976 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800977 '--env-prefix', default=[], action='append', nargs=2,
978 metavar='VAR local/path',
979 help='Prepend task-relative `local/path` to the task\'s VAR environment '
980 'variable using os-appropriate pathsep character. Can be specified '
981 'multiple times for the same VAR to add multiple paths.')
982 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400983 '--idempotent', action='store_true', default=False,
984 help='When set, the server will actively try to find a previous task '
985 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800986 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700987 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700988 help='The optional path to a file containing the secret_bytes to use with'
989 'this task.')
maruel681d6802017-01-17 16:56:03 -0800990 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700991 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400992 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800993 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700994 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400995 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800996 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500997 '--raw-cmd', action='store_true', default=False,
998 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700999 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -08001000 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001001 '--relative-cwd',
1002 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1003 'requires --raw-cmd')
1004 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001005 '--cipd-package', action='append', default=[], metavar='PKG',
1006 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -07001007 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001008 group.add_option(
1009 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -07001010 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001011 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1012 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001013 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001014 help='Email of a service account to run the task as, or literal "bot" '
1015 'string to indicate that the task should use the same account the '
1016 'bot itself is using to authenticate to Swarming. Don\'t use task '
1017 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001018 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001019 '--pool-task-template',
1020 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1021 default='AUTO',
1022 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
1023 'By default, the pool\'s TaskTemplate is automatically selected, '
1024 'according the pool configuration on the server. Choices are: '
1025 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
1026 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001027 '-o', '--output', action='append', default=[], metavar='PATH',
1028 help='A list of files to return in addition to those written to '
1029 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1030 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001031 group.add_option(
1032 '--wait-for-capacity', action='store_true', default=False,
1033 help='Instructs to leave the task PENDING even if there\'s no known bot '
1034 'that could run this task, otherwise the task will be denied with '
1035 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001036 parser.add_option_group(group)
1037
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001038 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001039 group.add_option(
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +00001040 '--priority', type='int', default=200,
maruel681d6802017-01-17 16:56:03 -08001041 help='The lower value, the more important the task is')
1042 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001043 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001044 help='Display name of the task. Defaults to '
1045 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1046 'isolated file is provided, if a hash is provided, it defaults to '
1047 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1048 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001049 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001050 help='Tags to assign to the task.')
1051 group.add_option(
1052 '--user', default='',
1053 help='User associated with the task. Defaults to authenticated user on '
1054 'the server.')
1055 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001056 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001057 help='Seconds to allow the task to be pending for a bot to run before '
1058 'this task request expires.')
1059 group.add_option(
1060 '--deadline', type='int', dest='expiration',
1061 help=optparse.SUPPRESS_HELP)
1062 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001063
1064
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001065def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001066 """Processes trigger options and does preparatory steps.
1067
1068 Returns:
1069 NewTaskRequest instance.
1070 """
maruelaf6b06c2017-06-08 06:26:53 -07001071 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001072 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001073 if args and args[0] == '--':
1074 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001075
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 if not options.dimensions:
1077 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001078 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1079 parser.error('--tags must be in the format key:value')
1080 if options.raw_cmd and not args:
1081 parser.error(
1082 'Arguments with --raw-cmd should be passed after -- as command '
1083 'delimiter.')
1084 if options.isolate_server and not options.namespace:
1085 parser.error(
1086 '--namespace must be a valid value when --isolate-server is used')
1087 if not options.isolated and not options.raw_cmd:
1088 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1089
1090 # Isolated
1091 # --isolated is required only if --raw-cmd wasn't provided.
1092 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1093 # preferred server.
1094 isolateserver.process_isolate_server_options(
1095 parser, options, False, not options.raw_cmd)
1096 inputs_ref = None
1097 if options.isolate_server:
1098 inputs_ref = FilesRef(
1099 isolated=options.isolated,
1100 isolatedserver=options.isolate_server,
1101 namespace=options.namespace)
1102
1103 # Command
1104 command = None
1105 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001106 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001107 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001108 if options.relative_cwd:
1109 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1110 if not a.startswith(os.getcwd()):
1111 parser.error(
1112 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001113 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001114 if options.relative_cwd:
1115 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001116 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001117
maruel0a25f6c2017-05-10 10:43:23 -07001118 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001119 cipd_packages = []
1120 for p in options.cipd_package:
1121 split = p.split(':', 2)
1122 if len(split) != 3:
1123 parser.error('CIPD packages must take the form: path:package:version')
1124 cipd_packages.append(CipdPackage(
1125 package_name=split[1],
1126 path=split[0],
1127 version=split[2]))
1128 cipd_input = None
1129 if cipd_packages:
1130 cipd_input = CipdInput(
1131 client_package=None,
1132 packages=cipd_packages,
1133 server=None)
1134
maruel0a25f6c2017-05-10 10:43:23 -07001135 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001136 secret_bytes = None
1137 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001138 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001139 secret_bytes = f.read().encode('base64')
1140
maruel0a25f6c2017-05-10 10:43:23 -07001141 # Named caches
maruel681d6802017-01-17 16:56:03 -08001142 caches = [
1143 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1144 for i in options.named_cache
1145 ]
maruel0a25f6c2017-05-10 10:43:23 -07001146
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001147 env_prefixes = {}
1148 for k, v in options.env_prefix:
1149 env_prefixes.setdefault(k, []).append(v)
1150
Brad Hallf78187a2018-10-19 17:08:55 +00001151 # Get dimensions into the key/value format we can manipulate later.
1152 orig_dims = [
1153 {'key': key, 'value': value} for key, value in options.dimensions]
1154 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1155
1156 # Construct base properties that we will use for all the slices, adding in
1157 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001158 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001159 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001160 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001161 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001162 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001163 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001164 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001165 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001166 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001167 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001168 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001169 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001170 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001171 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001172 outputs=options.output,
1173 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001174
1175 slices = []
1176
1177 # Group the optional dimensions by expiration.
1178 dims_by_exp = {}
1179 for key, value, exp_secs in options.optional_dimensions:
1180 dims_by_exp.setdefault(int(exp_secs), []).append(
1181 {'key': key, 'value': value})
1182
1183 # Create the optional slices with expiration deltas, we fix up the properties
1184 # below.
1185 last_exp = 0
1186 for expiration_secs in sorted(dims_by_exp):
1187 t = TaskSlice(
1188 expiration_secs=expiration_secs - last_exp,
1189 properties=properties,
1190 wait_for_capacity=False)
1191 slices.append(t)
1192 last_exp = expiration_secs
1193
1194 # Add back in the default slice (the last one).
1195 exp = max(int(options.expiration) - last_exp, 60)
1196 base_task_slice = TaskSlice(
1197 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001198 properties=properties,
1199 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001200 slices.append(base_task_slice)
1201
Brad Hall7f463e62018-11-16 16:13:30 +00001202 # Add optional dimensions to the task slices, replacing a dimension that
1203 # has the same key if it is a dimension where repeating isn't valid (otherwise
1204 # we append it). Currently the only dimension we can repeat is "caches"; the
1205 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001206 extra_dims = []
Brad Hall7f463e62018-11-16 16:13:30 +00001207 for i, (_, kvs) in enumerate(sorted(dims_by_exp.iteritems(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001208 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001209 # Replace or append the key/value pairs for this expiration in extra_dims;
1210 # we keep extra_dims around because we are iterating backwards and filling
1211 # in slices with shorter expirations. Dimensions expire as time goes on so
1212 # the slices that expire earlier will generally have more dimensions.
1213 for kv in kvs:
1214 if kv['key'] == 'caches':
1215 extra_dims.append(kv)
1216 else:
1217 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1218 # Then, add all the optional dimensions to the original dimension set, again
1219 # replacing if needed.
1220 for kv in extra_dims:
1221 if kv['key'] == 'caches':
1222 dims.append(kv)
1223 else:
1224 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001225 dims.sort(key=lambda x: (x['key'], x['value']))
1226 slice_properties = properties._replace(dimensions=dims)
1227 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1228
maruel77f720b2015-09-15 12:35:22 -07001229 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001230 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001231 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001232 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001233 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001234 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001235 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001236 user=options.user,
1237 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001238
1239
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001240class TaskOutputStdoutOption(optparse.Option):
1241 """Where to output the each task's console output (stderr/stdout).
1242
1243 The output will be;
1244 none - not be downloaded.
1245 json - stored in summary.json file *only*.
1246 console - shown on stdout *only*.
1247 all - stored in summary.json and shown on stdout.
1248 """
1249
1250 choices = ['all', 'json', 'console', 'none']
1251
1252 def __init__(self, *args, **kw):
1253 optparse.Option.__init__(
1254 self,
1255 *args,
1256 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001257 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001258 help=re.sub('\s\s*', ' ', self.__doc__),
1259 **kw)
1260
1261 def convert_value(self, opt, value):
1262 if value not in self.choices:
1263 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1264 self.get_opt_string(), self.choices, value))
1265 stdout_to = []
1266 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001267 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001268 elif value != 'none':
1269 stdout_to = [value]
1270 return stdout_to
1271
1272
maruel@chromium.org0437a732013-08-27 16:05:52 +00001273def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001274 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001275 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001276 help='Timeout to wait for result, set to -1 for no timeout and get '
1277 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001278 parser.group_logging.add_option(
1279 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001280 parser.group_logging.add_option(
1281 '--print-status-updates', action='store_true',
1282 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001283 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001284 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001285 '--task-summary-json',
1286 metavar='FILE',
1287 help='Dump a summary of task results to this file as json. It contains '
1288 'only shards statuses as know to server directly. Any output files '
1289 'emitted by the task can be collected by using --task-output-dir')
1290 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001291 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001292 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001293 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001294 'directory contains per-shard directory with output files produced '
1295 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001296 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001297 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001298 parser.task_output_group.add_option(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001299 '--filepath-filter',
1300 help='This is regexp filter used to specify downloaded filepath when '
1301 'collecting isolated output.')
1302 parser.task_output_group.add_option(
maruel9531ce02016-04-13 06:11:23 -07001303 '--perf', action='store_true', default=False,
1304 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001305 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001306
1307
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001308def process_collect_options(parser, options):
1309 # Only negative -1 is allowed, disallow other negative values.
1310 if options.timeout != -1 and options.timeout < 0:
1311 parser.error('Invalid --timeout value')
1312
1313
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001314@subcommand.usage('bots...')
1315def CMDbot_delete(parser, args):
1316 """Forcibly deletes bots from the Swarming server."""
1317 parser.add_option(
1318 '-f', '--force', action='store_true',
1319 help='Do not prompt for confirmation')
1320 options, args = parser.parse_args(args)
1321 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001322 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001323
1324 bots = sorted(args)
1325 if not options.force:
1326 print('Delete the following bots?')
1327 for bot in bots:
1328 print(' %s' % bot)
1329 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1330 print('Goodbye.')
1331 return 1
1332
1333 result = 0
1334 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001335 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001336 if net.url_read_json(url, data={}, method='POST') is None:
1337 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001338 result = 1
1339 return result
1340
1341
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001342def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001343 """Returns information about the bots connected to the Swarming server."""
1344 add_filter_options(parser)
1345 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001346 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001347 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001348 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001349 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001350 help='Keep both dead and alive bots')
1351 parser.filter_group.add_option(
1352 '--busy', action='store_true', help='Keep only busy bots')
1353 parser.filter_group.add_option(
1354 '--idle', action='store_true', help='Keep only idle bots')
1355 parser.filter_group.add_option(
1356 '--mp', action='store_true',
1357 help='Keep only Machine Provider managed bots')
1358 parser.filter_group.add_option(
1359 '--non-mp', action='store_true',
1360 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001361 parser.filter_group.add_option(
1362 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001363 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001364 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001365 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001366
1367 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001368 parser.error('Use only one of --keep-dead or --dead-only')
1369 if options.busy and options.idle:
1370 parser.error('Use only one of --busy or --idle')
1371 if options.mp and options.non_mp:
1372 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001373
smut281c3902018-05-30 17:50:05 -07001374 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001375 values = []
1376 if options.dead_only:
1377 values.append(('is_dead', 'TRUE'))
1378 elif options.keep_dead:
1379 values.append(('is_dead', 'NONE'))
1380 else:
1381 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001382
maruelaf6b06c2017-06-08 06:26:53 -07001383 if options.busy:
1384 values.append(('is_busy', 'TRUE'))
1385 elif options.idle:
1386 values.append(('is_busy', 'FALSE'))
1387 else:
1388 values.append(('is_busy', 'NONE'))
1389
1390 if options.mp:
1391 values.append(('is_mp', 'TRUE'))
1392 elif options.non_mp:
1393 values.append(('is_mp', 'FALSE'))
1394 else:
1395 values.append(('is_mp', 'NONE'))
1396
1397 for key, value in options.dimensions:
1398 values.append(('dimensions', '%s:%s' % (key, value)))
1399 url += urllib.urlencode(values)
1400 try:
1401 data, yielder = get_yielder(url, 0)
1402 bots = data.get('items') or []
1403 for items in yielder():
1404 if items:
1405 bots.extend(items)
1406 except Failure as e:
1407 sys.stderr.write('\n%s\n' % e)
1408 return 1
maruel77f720b2015-09-15 12:35:22 -07001409 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001410 print bot['bot_id']
1411 if not options.bare:
1412 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1413 print ' %s' % json.dumps(dimensions, sort_keys=True)
1414 if bot.get('task_id'):
1415 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001416 return 0
1417
1418
maruelfd0a90c2016-06-10 11:51:10 -07001419@subcommand.usage('task_id')
1420def CMDcancel(parser, args):
1421 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001422 parser.add_option(
1423 '-k', '--kill-running', action='store_true', default=False,
1424 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001425 options, args = parser.parse_args(args)
1426 if not args:
1427 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001428 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001429 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001430 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001431 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001432 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001433 print('Deleting %s failed. Probably already gone' % task_id)
1434 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001435 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001436 return 0
1437
1438
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001439@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001440def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001441 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001442
1443 The result can be in multiple part if the execution was sharded. It can
1444 potentially have retries.
1445 """
1446 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001447 parser.add_option(
1448 '-j', '--json',
1449 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001450 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001451 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001452 if not args and not options.json:
1453 parser.error('Must specify at least one task id or --json.')
1454 if args and options.json:
1455 parser.error('Only use one of task id or --json.')
1456
1457 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001458 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001459 try:
maruel1ceb3872015-10-14 06:10:44 -07001460 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001461 data = json.load(f)
1462 except (IOError, ValueError):
1463 parser.error('Failed to open %s' % options.json)
1464 try:
1465 tasks = sorted(
1466 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1467 args = [t['task_id'] for t in tasks]
1468 except (KeyError, TypeError):
1469 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001470 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001471 # Take in account all the task slices.
1472 offset = 0
1473 for s in data['request']['task_slices']:
1474 m = (offset + s['properties']['execution_timeout_secs'] +
1475 s['expiration_secs'])
1476 if m > options.timeout:
1477 options.timeout = m
1478 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001479 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001480 else:
1481 valid = frozenset('0123456789abcdef')
1482 if any(not valid.issuperset(task_id) for task_id in args):
1483 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001484
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001485 try:
1486 return collect(
1487 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001488 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001489 options.timeout,
1490 options.decorate,
1491 options.print_status_updates,
1492 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001493 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001494 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001495 options.perf,
1496 options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001497 except Failure:
1498 on_error.report(None)
1499 return 1
1500
1501
maruel77f720b2015-09-15 12:35:22 -07001502@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001503def CMDpost(parser, args):
1504 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1505
1506 Input data must be sent to stdin, result is printed to stdout.
1507
1508 If HTTP response code >= 400, returns non-zero.
1509 """
1510 options, args = parser.parse_args(args)
1511 if len(args) != 1:
1512 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001513 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001514 data = sys.stdin.read()
1515 try:
1516 resp = net.url_read(url, data=data, method='POST')
1517 except net.TimeoutError:
1518 sys.stderr.write('Timeout!\n')
1519 return 1
1520 if not resp:
1521 sys.stderr.write('No response!\n')
1522 return 1
1523 sys.stdout.write(resp)
1524 return 0
1525
1526
1527@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001528def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001529 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1530 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001531
1532 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001533 Raw task request and results:
1534 swarming.py query -S server-url.com task/123456/request
1535 swarming.py query -S server-url.com task/123456/result
1536
maruel77f720b2015-09-15 12:35:22 -07001537 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001538 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001539
maruelaf6b06c2017-06-08 06:26:53 -07001540 Listing last 10 tasks on a specific bot named 'bot1':
1541 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001542
maruelaf6b06c2017-06-08 06:26:53 -07001543 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001544 quoting is important!:
1545 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001546 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001547 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001548 parser.add_option(
1549 '-L', '--limit', type='int', default=200,
1550 help='Limit to enforce on limitless items (like number of tasks); '
1551 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001552 parser.add_option(
1553 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001554 parser.add_option(
1555 '--progress', action='store_true',
1556 help='Prints a dot at each request to show progress')
1557 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001558 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001559 parser.error(
1560 'Must specify only method name and optionally query args properly '
1561 'escaped.')
smut281c3902018-05-30 17:50:05 -07001562 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001563 try:
1564 data, yielder = get_yielder(base_url, options.limit)
1565 for items in yielder():
1566 if items:
1567 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001568 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001569 sys.stderr.write('.')
1570 sys.stderr.flush()
1571 except Failure as e:
1572 sys.stderr.write('\n%s\n' % e)
1573 return 1
maruel77f720b2015-09-15 12:35:22 -07001574 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001575 sys.stderr.write('\n')
1576 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001577 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001578 options.json = unicode(os.path.abspath(options.json))
1579 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001580 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001581 try:
maruel77f720b2015-09-15 12:35:22 -07001582 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001583 sys.stdout.write('\n')
1584 except IOError:
1585 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001586 return 0
1587
1588
maruel77f720b2015-09-15 12:35:22 -07001589def CMDquery_list(parser, args):
1590 """Returns list of all the Swarming APIs that can be used with command
1591 'query'.
1592 """
1593 parser.add_option(
1594 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1595 options, args = parser.parse_args(args)
1596 if args:
1597 parser.error('No argument allowed.')
1598
1599 try:
1600 apis = endpoints_api_discovery_apis(options.swarming)
1601 except APIError as e:
1602 parser.error(str(e))
1603 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001604 options.json = unicode(os.path.abspath(options.json))
1605 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001606 json.dump(apis, f)
1607 else:
1608 help_url = (
1609 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1610 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001611 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1612 if i:
1613 print('')
maruel77f720b2015-09-15 12:35:22 -07001614 print api_id
maruel11e31af2017-02-15 07:30:50 -08001615 print ' ' + api['description'].strip()
1616 if 'resources' in api:
1617 # Old.
1618 for j, (resource_name, resource) in enumerate(
1619 sorted(api['resources'].iteritems())):
1620 if j:
1621 print('')
1622 for method_name, method in sorted(resource['methods'].iteritems()):
1623 # Only list the GET ones.
1624 if method['httpMethod'] != 'GET':
1625 continue
1626 print '- %s.%s: %s' % (
1627 resource_name, method_name, method['path'])
1628 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001629 ' ' + l for l in textwrap.wrap(
1630 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001631 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1632 else:
1633 # New.
1634 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001635 # Only list the GET ones.
1636 if method['httpMethod'] != 'GET':
1637 continue
maruel11e31af2017-02-15 07:30:50 -08001638 print '- %s: %s' % (method['id'], method['path'])
1639 print('\n'.join(
1640 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001641 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1642 return 0
1643
1644
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001645@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001646def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001647 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001648
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001649 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001650 """
1651 add_trigger_options(parser)
1652 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001653 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001654 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001655 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001656 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001657 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001658 tasks = trigger_task_shards(
1659 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001660 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001661 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001662 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001663 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001664 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001665 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001666 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001667 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001668 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001669 task_ids = [
1670 t['task_id']
1671 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1672 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001673 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001674 offset = 0
1675 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001676 m = (offset + s.properties.execution_timeout_secs +
1677 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001678 if m > options.timeout:
1679 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001680 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001681 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001682 try:
1683 return collect(
1684 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001685 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001686 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001687 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001688 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001689 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001690 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001691 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001692 options.perf,
1693 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001694 except Failure:
1695 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001696 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001697
1698
maruel18122c62015-10-23 06:31:23 -07001699@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001700def CMDreproduce(parser, args):
1701 """Runs a task locally that was triggered on the server.
1702
1703 This running locally the same commands that have been run on the bot. The data
1704 downloaded will be in a subdirectory named 'work' of the current working
1705 directory.
maruel18122c62015-10-23 06:31:23 -07001706
1707 You can pass further additional arguments to the target command by passing
1708 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001709 """
maruelc070e672016-02-22 17:32:57 -08001710 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001711 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001712 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001713 parser.add_option(
1714 '--work', metavar='DIR', default='work',
1715 help='Directory to map the task input files into')
1716 parser.add_option(
1717 '--cache', metavar='DIR', default='cache',
1718 help='Directory that contains the input cache')
1719 parser.add_option(
1720 '--leak', action='store_true',
1721 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001722 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001723 extra_args = []
1724 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001725 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001726 if len(args) > 1:
1727 if args[1] == '--':
1728 if len(args) > 2:
1729 extra_args = args[2:]
1730 else:
1731 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001732
smut281c3902018-05-30 17:50:05 -07001733 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001734 request = net.url_read_json(url)
1735 if not request:
1736 print >> sys.stderr, 'Failed to retrieve request data for the task'
1737 return 1
1738
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001739 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001740 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001741 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001742 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001743 cachedir = unicode(os.path.abspath('cipd_cache'))
1744 if not fs.exists(cachedir):
1745 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001746
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001747 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001748 env = os.environ.copy()
1749 env['SWARMING_BOT_ID'] = 'reproduce'
1750 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001751 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001752 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001753 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001754 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001755 if not i['value']:
1756 env.pop(key, None)
1757 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001758 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001759
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001760 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001761 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001762 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001763 for i in env_prefixes:
1764 key = i['key']
1765 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001766 cur = env.get(key)
1767 if cur:
1768 paths.append(cur)
1769 env[key] = os.path.pathsep.join(paths)
1770
iannucci31ab9192017-05-02 19:11:56 -07001771 command = []
nodir152cba62016-05-12 16:08:56 -07001772 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001773 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001774 server_ref = isolate_storage.ServerRef(
maruel29ab2fd2015-10-16 11:44:01 -07001775 properties['inputs_ref']['isolatedserver'],
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001776 properties['inputs_ref']['namespace'])
1777 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001778 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1779 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1780 # leak.
1781 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001782 cache = local_caching.DiskContentAddressedCache(
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001783 unicode(os.path.abspath(options.cache)), policies,
1784 server_ref.hash_algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001785 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001786 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001787 command = bundle.command
1788 if bundle.relative_cwd:
1789 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001790 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001791
1792 if properties.get('command'):
1793 command.extend(properties['command'])
1794
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001795 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001796 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001797 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001798 new_command = run_isolated.process_command(command, 'invalid', None)
1799 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001800 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001801 else:
1802 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001803 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001804 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001805 command, options.output, None)
1806 if not os.path.isdir(options.output):
1807 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001808 command = new_command
1809 file_path.ensure_command_has_abs_path(command, workdir)
1810
1811 if properties.get('cipd_input'):
1812 ci = properties['cipd_input']
1813 cp = ci['client_package']
1814 client_manager = cipd.get_client(
1815 ci['server'], cp['package_name'], cp['version'], cachedir)
1816
1817 with client_manager as client:
1818 by_path = collections.defaultdict(list)
1819 for pkg in ci['packages']:
1820 path = pkg['path']
1821 # cipd deals with 'root' as ''
1822 if path == '.':
1823 path = ''
1824 by_path[path].append((pkg['package_name'], pkg['version']))
1825 client.ensure(workdir, by_path, cache_dir=cachedir)
1826
maruel77f720b2015-09-15 12:35:22 -07001827 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001828 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001829 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001830 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001831 print >> sys.stderr, str(e)
1832 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001833 finally:
1834 # Do not delete options.cache.
1835 if not options.leak:
1836 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001837
1838
maruel0eb1d1b2015-10-02 14:48:21 -07001839@subcommand.usage('bot_id')
1840def CMDterminate(parser, args):
1841 """Tells a bot to gracefully shut itself down as soon as it can.
1842
1843 This is done by completing whatever current task there is then exiting the bot
1844 process.
1845 """
1846 parser.add_option(
1847 '--wait', action='store_true', help='Wait for the bot to terminate')
1848 options, args = parser.parse_args(args)
1849 if len(args) != 1:
1850 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001851 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001852 request = net.url_read_json(url, data={})
1853 if not request:
1854 print >> sys.stderr, 'Failed to ask for termination'
1855 return 1
1856 if options.wait:
1857 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001858 options.swarming,
1859 [request['task_id']],
1860 0.,
1861 False,
1862 False,
1863 None,
1864 None,
1865 [],
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001866 False,
1867 None)
maruelbfc5f872017-06-10 16:43:17 -07001868 else:
1869 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001870 return 0
1871
1872
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001873@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001874def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001875 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001876
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001877 Passes all extra arguments provided after '--' as additional command line
1878 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001879 """
1880 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001881 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001882 parser.add_option(
1883 '--dump-json',
1884 metavar='FILE',
1885 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001886 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001887 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001888 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001889 tasks = trigger_task_shards(
1890 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001891 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001892 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001893 tasks_sorted = sorted(
1894 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001895 if options.dump_json:
1896 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001897 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001898 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001899 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001900 }
maruel46b015f2015-10-13 18:40:35 -07001901 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001902 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001903 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001904 (options.swarming, options.dump_json))
1905 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001906 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001907 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001908 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1909 print('Or visit:')
1910 for t in tasks_sorted:
1911 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001912 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001913 except Failure:
1914 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001915 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001916
1917
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001918class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001919 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001920 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001921 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001922 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001923 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001924 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001925 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001926 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001927 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001928 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001929
1930 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001931 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001932 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001933 auth.process_auth_options(self, options)
1934 user = self._process_swarming(options)
1935 if hasattr(options, 'user') and not options.user:
1936 options.user = user
1937 return options, args
1938
1939 def _process_swarming(self, options):
1940 """Processes the --swarming option and aborts if not specified.
1941
1942 Returns the identity as determined by the server.
1943 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001944 if not options.swarming:
1945 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001946 try:
1947 options.swarming = net.fix_url(options.swarming)
1948 except ValueError as e:
1949 self.error('--swarming %s' % e)
1950 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001951 try:
1952 user = auth.ensure_logged_in(options.swarming)
1953 except ValueError as e:
1954 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001955 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001956
1957
1958def main(args):
1959 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001960 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001961
1962
1963if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001964 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001965 fix_encoding.fix_encoding()
1966 tools.disable_buffering()
1967 colorama.init()
1968 sys.exit(main(sys.argv[1:]))