blob: 88e77404f822007f8d23cd70e9e9393374872594 [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:
Brad Hall157bec82018-11-26 22:15:38 +0000246 for task_slice in req['task_slices']:
247 task_slice['properties']['env'] = setup_googletest(
248 task_slice['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700249 req['name'] += ':%s:%s' % (index, shards)
250 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251
252 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500253 tasks = {}
254 priority_warning = False
255 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700256 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500257 if not task:
258 break
259 logging.info('Request result: %s', task)
260 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400261 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500262 priority_warning = True
263 print >> sys.stderr, (
264 'Priority was reset to %s' % task['request']['priority'])
265 tasks[request['name']] = {
266 'shard_index': index,
267 'task_id': task['task_id'],
268 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
269 }
270
271 # Some shards weren't triggered. Abort everything.
272 if len(tasks) != len(requests):
273 if tasks:
274 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
275 len(tasks), len(requests))
276 for task_dict in tasks.itervalues():
277 abort_task(swarming, task_dict['task_id'])
278 return None
279
280 return tasks
281
282
283### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000284
285
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700286# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000287STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700288
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400289
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000290class TaskState(object):
291 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000292
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000293 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
294 is the source of truth for these values:
295 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400296
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000297 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400298 """
299 RUNNING = 0x10
300 PENDING = 0x20
301 EXPIRED = 0x30
302 TIMED_OUT = 0x40
303 BOT_DIED = 0x50
304 CANCELED = 0x60
305 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400306 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400307 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400308
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000309 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400310
maruel77f720b2015-09-15 12:35:22 -0700311 _ENUMS = {
312 'RUNNING': RUNNING,
313 'PENDING': PENDING,
314 'EXPIRED': EXPIRED,
315 'TIMED_OUT': TIMED_OUT,
316 'BOT_DIED': BOT_DIED,
317 'CANCELED': CANCELED,
318 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400319 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400320 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700321 }
322
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400323 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700324 def from_enum(cls, state):
325 """Returns int value based on the string."""
326 if state not in cls._ENUMS:
327 raise ValueError('Invalid state %s' % state)
328 return cls._ENUMS[state]
329
maruel@chromium.org0437a732013-08-27 16:05:52 +0000330
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700331class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700332 """Assembles task execution summary (for --task-summary-json output).
333
334 Optionally fetches task outputs from isolate server to local disk (used when
335 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700336
337 This object is shared among multiple threads running 'retrieve_results'
338 function, in particular they call 'process_shard_result' method in parallel.
339 """
340
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000341 def __init__(self, task_output_dir, task_output_stdout, shard_count,
342 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700343 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
344
345 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700346 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700347 shard_count: expected number of task shards.
348 """
maruel12e30012015-10-09 11:55:35 -0700349 self.task_output_dir = (
350 unicode(os.path.abspath(task_output_dir))
351 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000352 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000354 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700355
356 self._lock = threading.Lock()
357 self._per_shard_results = {}
358 self._storage = None
359
nodire5028a92016-04-29 14:38:21 -0700360 if self.task_output_dir:
361 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362
Vadim Shtayurab450c602014-05-12 19:23:25 -0700363 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364 """Stores results of a single task shard, fetches output files if necessary.
365
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400366 Modifies |result| in place.
367
maruel77f720b2015-09-15 12:35:22 -0700368 shard_index is 0-based.
369
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370 Called concurrently from multiple threads.
371 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700373 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374 if shard_index < 0 or shard_index >= self.shard_count:
375 logging.warning(
376 'Shard index %d is outside of expected range: [0; %d]',
377 shard_index, self.shard_count - 1)
378 return
379
maruel77f720b2015-09-15 12:35:22 -0700380 if result.get('outputs_ref'):
381 ref = result['outputs_ref']
382 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
383 ref['isolatedserver'],
384 urllib.urlencode(
385 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400386
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 # Store result dict of that shard, ignore results we've already seen.
388 with self._lock:
389 if shard_index in self._per_shard_results:
390 logging.warning('Ignoring duplicate shard index %d', shard_index)
391 return
392 self._per_shard_results[shard_index] = result
393
394 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700395 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000396 server_ref = isolate_storage.ServerRef(
397 result['outputs_ref']['isolatedserver'],
398 result['outputs_ref']['namespace'])
399 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400400 if storage:
401 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400402 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
403 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400404 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700405 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400406 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400407 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700408 os.path.join(self.task_output_dir, str(shard_index)),
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000409 False, self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700410
411 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700412 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700413 with self._lock:
414 # Write an array of shard results with None for missing shards.
415 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700416 'shards': [
417 self._per_shard_results.get(i) for i in xrange(self.shard_count)
418 ],
419 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000420
421 # Don't store stdout in the summary if not requested too.
422 if "json" not in self.task_output_stdout:
423 for shard_json in summary['shards']:
424 if not shard_json:
425 continue
426 if "output" in shard_json:
427 del shard_json["output"]
428 if "outputs" in shard_json:
429 del shard_json["outputs"]
430
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700431 # Write summary.json to task_output_dir as well.
432 if self.task_output_dir:
433 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700434 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700435 summary,
436 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700437 if self._storage:
438 self._storage.close()
439 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000442 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700444 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 with self._lock:
446 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000447 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 else:
449 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000450 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700451 logging.error(
452 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000453 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000455 if self._storage.server_ref.namespace != server_ref.namespace:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 logging.error(
457 'Task shards are using multiple namespaces: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000458 self._storage.server_ref.namespace, server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700459 return None
460 return self._storage
461
462
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500463def now():
464 """Exists so it can be mocked easily."""
465 return time.time()
466
467
maruel77f720b2015-09-15 12:35:22 -0700468def parse_time(value):
469 """Converts serialized time from the API to datetime.datetime."""
470 # When microseconds are 0, the '.123456' suffix is elided. This means the
471 # serialized format is not consistent, which confuses the hell out of python.
472 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
473 try:
474 return datetime.datetime.strptime(value, fmt)
475 except ValueError:
476 pass
477 raise ValueError('Failed to parse %s' % value)
478
479
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700480def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700481 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000482 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400483 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700484
Vadim Shtayurab450c602014-05-12 19:23:25 -0700485 Returns:
486 <result dict> on success.
487 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700488 """
maruel71c61c82016-02-22 06:52:05 -0800489 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700490 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700491 if include_perf:
492 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700493 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400495 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700496 attempt = 0
497
498 while not should_stop.is_set():
499 attempt += 1
500
501 # Waiting for too long -> give up.
502 current_time = now()
503 if deadline and current_time >= deadline:
504 logging.error('retrieve_results(%s) timed out on attempt %d',
505 base_url, attempt)
506 return None
507
508 # Do not spin too fast. Spin faster at the beginning though.
509 # Start with 1 sec delay and for each 30 sec of waiting add another second
510 # of delay, until hitting 15 sec ceiling.
511 if attempt > 1:
512 max_delay = min(15, 1 + (current_time - started) / 30.0)
513 delay = min(max_delay, deadline - current_time) if deadline else max_delay
514 if delay > 0:
515 logging.debug('Waiting %.1f sec before retrying', delay)
516 should_stop.wait(delay)
517 if should_stop.is_set():
518 return None
519
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400520 # Disable internal retries in net.url_read_json, since we are doing retries
521 # ourselves.
522 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700523 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
524 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400525 # Retry on 500s only if no timeout is specified.
526 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400527 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400528 if timeout == -1:
529 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400530 continue
maruel77f720b2015-09-15 12:35:22 -0700531
maruelbf53e042015-12-01 15:00:51 -0800532 if result.get('error'):
533 # An error occurred.
534 if result['error'].get('errors'):
535 for err in result['error']['errors']:
536 logging.warning(
537 'Error while reading task: %s; %s',
538 err.get('message'), err.get('debugInfo'))
539 elif result['error'].get('message'):
540 logging.warning(
541 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400542 if timeout == -1:
543 return result
maruelbf53e042015-12-01 15:00:51 -0800544 continue
545
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400546 # When timeout == -1, always return on first attempt. 500s are already
547 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000548 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000549 if fetch_stdout:
550 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700551 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700552 # Record the result, try to fetch attached output files (if any).
553 if output_collector:
554 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700555 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700556 if result.get('internal_failure'):
557 logging.error('Internal error!')
558 elif result['state'] == 'BOT_DIED':
559 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700560 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000561
562
maruel77f720b2015-09-15 12:35:22 -0700563def convert_to_old_format(result):
564 """Converts the task result data from Endpoints API format to old API format
565 for compatibility.
566
567 This goes into the file generated as --task-summary-json.
568 """
569 # Sets default.
570 result.setdefault('abandoned_ts', None)
571 result.setdefault('bot_id', None)
572 result.setdefault('bot_version', None)
573 result.setdefault('children_task_ids', [])
574 result.setdefault('completed_ts', None)
575 result.setdefault('cost_saved_usd', None)
576 result.setdefault('costs_usd', None)
577 result.setdefault('deduped_from', None)
578 result.setdefault('name', None)
579 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700580 result.setdefault('server_versions', None)
581 result.setdefault('started_ts', None)
582 result.setdefault('tags', None)
583 result.setdefault('user', None)
584
585 # Convertion back to old API.
586 duration = result.pop('duration', None)
587 result['durations'] = [duration] if duration else []
588 exit_code = result.pop('exit_code', None)
589 result['exit_codes'] = [int(exit_code)] if exit_code else []
590 result['id'] = result.pop('task_id')
591 result['isolated_out'] = result.get('outputs_ref', None)
592 output = result.pop('output', None)
593 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700594 # server_version
595 # Endpoints result 'state' as string. For compatibility with old code, convert
596 # to int.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000597 result['state'] = TaskState.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700598 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700599 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700600 if 'bot_dimensions' in result:
601 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700602 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700603 }
604 else:
605 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700606
607
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700608def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400609 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000610 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500611 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000612
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700613 Duplicate shards are ignored. Shards are yielded in order of completion.
614 Timed out shards are NOT yielded at all. Caller can compare number of yielded
615 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000616
617 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500618 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 +0000619 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500620
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700621 output_collector is an optional instance of TaskOutputCollector that will be
622 used to fetch files produced by a task from isolate server to the local disk.
623
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500624 Yields:
625 (index, result). In particular, 'result' is defined as the
626 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000627 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400629 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700630 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700631 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700632
maruel@chromium.org0437a732013-08-27 16:05:52 +0000633 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
634 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700635 # Adds a task to the thread pool to call 'retrieve_results' and return
636 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400637 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000638 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000642 task_id, timeout, should_stop, output_collector, include_perf,
643 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700644
645 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400646 for shard_index, task_id in enumerate(task_ids):
647 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700648
649 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400650 shards_remaining = range(len(task_ids))
651 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700652 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700653 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700654 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700655 shard_index, result = results_channel.pull(
656 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700657 except threading_utils.TaskChannel.Timeout:
658 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000659 time_now = str(datetime.datetime.now())
660 _, time_now = time_now.split(' ')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700661 print(
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000662 '%s '
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 'Waiting for results from the following shards: %s' %
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000664 (time_now, ', '.join(map(str, shards_remaining)))
665 )
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700666 sys.stdout.flush()
667 continue
668 except Exception:
669 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700670
671 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700672 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000673 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500674 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700676
Vadim Shtayurab450c602014-05-12 19:23:25 -0700677 # Yield back results to the caller.
678 assert shard_index in shards_remaining
679 shards_remaining.remove(shard_index)
680 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700681
maruel@chromium.org0437a732013-08-27 16:05:52 +0000682 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700683 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000684 should_stop.set()
685
686
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000687def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000688 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700689 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400690 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700691 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
692 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400693 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
694 metadata.get('abandoned_ts')):
695 pending = '%.1fs' % (
696 parse_time(metadata['abandoned_ts']) -
697 parse_time(metadata['created_ts'])
698 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400699 else:
700 pending = 'N/A'
701
maruel77f720b2015-09-15 12:35:22 -0700702 if metadata.get('duration') is not None:
703 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400704 else:
705 duration = 'N/A'
706
maruel77f720b2015-09-15 12:35:22 -0700707 if metadata.get('exit_code') is not None:
708 # Integers are encoded as string to not loose precision.
709 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400710 else:
711 exit_code = 'N/A'
712
713 bot_id = metadata.get('bot_id') or 'N/A'
714
maruel77f720b2015-09-15 12:35:22 -0700715 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400716 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000717 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400718 if metadata.get('state') == 'CANCELED':
719 tag_footer2 = ' Pending: %s CANCELED' % pending
720 elif metadata.get('state') == 'EXPIRED':
721 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400722 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400723 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
724 pending, duration, bot_id, exit_code, metadata['state'])
725 else:
726 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
727 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400728
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000729 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
730 dash_pad = '+-%s-+' % ('-' * tag_len)
731 tag_header = '| %s |' % tag_header.ljust(tag_len)
732 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
733 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400734
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000735 if include_stdout:
736 return '\n'.join([
737 dash_pad,
738 tag_header,
739 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400740 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000741 dash_pad,
742 tag_footer1,
743 tag_footer2,
744 dash_pad,
745 ])
746 else:
747 return '\n'.join([
748 dash_pad,
749 tag_header,
750 tag_footer2,
751 dash_pad,
752 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000753
754
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700755def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700756 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000757 task_summary_json, task_output_dir, task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000758 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700759 """Retrieves results of a Swarming task.
760
761 Returns:
762 process exit code that should be returned to the user.
763 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000764
765 filter_cb = None
766 if filepath_filter:
767 filter_cb = re.compile(filepath_filter).match
768
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700769 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000770 output_collector = TaskOutputCollector(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000771 task_output_dir, task_output_stdout, len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700772
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700773 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700774 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400775 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700776 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400777 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400778 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000779 output_collector, include_perf,
780 (len(task_output_stdout) > 0),
781 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700783
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400784 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700785 shard_exit_code = metadata.get('exit_code')
786 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700787 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700788 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700789 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400790 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700791 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700792
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700793 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000794 s = decorate_shard_output(
795 swarming, index, metadata,
796 "console" in task_output_stdout).encode(
797 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700798 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400799 if len(seen_shards) < len(task_ids):
800 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700801 else:
maruel77f720b2015-09-15 12:35:22 -0700802 print('%s: %s %s' % (
803 metadata.get('bot_id', 'N/A'),
804 metadata['task_id'],
805 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000806 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700807 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400808 if output:
809 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700810 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700811 summary = output_collector.finalize()
812 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700813 # TODO(maruel): Make this optional.
814 for i in summary['shards']:
815 if i:
816 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700817 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700818
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400819 if decorate and total_duration:
820 print('Total duration: %.1fs' % total_duration)
821
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400822 if len(seen_shards) != len(task_ids):
823 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700824 print >> sys.stderr, ('Results from some shards are missing: %s' %
825 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700826 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700827
maruela5490782015-09-30 10:56:59 -0700828 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000829
830
maruel77f720b2015-09-15 12:35:22 -0700831### API management.
832
833
834class APIError(Exception):
835 pass
836
837
838def endpoints_api_discovery_apis(host):
839 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
840 the APIs exposed by a host.
841
842 https://developers.google.com/discovery/v1/reference/apis/list
843 """
maruel380e3262016-08-31 16:10:06 -0700844 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
845 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700846 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
847 if data is None:
848 raise APIError('Failed to discover APIs on %s' % host)
849 out = {}
850 for api in data['items']:
851 if api['id'] == 'discovery:v1':
852 continue
853 # URL is of the following form:
854 # url = host + (
855 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
856 api_data = net.url_read_json(api['discoveryRestUrl'])
857 if api_data is None:
858 raise APIError('Failed to discover %s on %s' % (api['id'], host))
859 out[api['id']] = api_data
860 return out
861
862
maruelaf6b06c2017-06-08 06:26:53 -0700863def get_yielder(base_url, limit):
864 """Returns the first query and a function that yields following items."""
865 CHUNK_SIZE = 250
866
867 url = base_url
868 if limit:
869 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
870 data = net.url_read_json(url)
871 if data is None:
872 # TODO(maruel): Do basic diagnostic.
873 raise Failure('Failed to access %s' % url)
874 org_cursor = data.pop('cursor', None)
875 org_total = len(data.get('items') or [])
876 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
877 if not org_cursor or not org_total:
878 # This is not an iterable resource.
879 return data, lambda: []
880
881 def yielder():
882 cursor = org_cursor
883 total = org_total
884 # Some items support cursors. Try to get automatically if cursors are needed
885 # by looking at the 'cursor' items.
886 while cursor and (not limit or total < limit):
887 merge_char = '&' if '?' in base_url else '?'
888 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
889 if limit:
890 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
891 new = net.url_read_json(url)
892 if new is None:
893 raise Failure('Failed to access %s' % url)
894 cursor = new.get('cursor')
895 new_items = new.get('items')
896 nb_items = len(new_items or [])
897 total += nb_items
898 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
899 yield new_items
900
901 return data, yielder
902
903
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500904### Commands.
905
906
907def abort_task(_swarming, _manifest):
908 """Given a task manifest that was triggered, aborts its execution."""
909 # TODO(vadimsh): No supported by the server yet.
910
911
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400912def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800913 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500914 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500915 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500916 dest='dimensions', metavar='FOO bar',
917 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000918 parser.filter_group.add_option(
919 '--optional-dimension', default=[], action='append', nargs=3,
920 dest='optional_dimensions', metavar='key value expiration',
921 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500922 parser.add_option_group(parser.filter_group)
923
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400924
Brad Hallf78187a2018-10-19 17:08:55 +0000925def _validate_filter_option(parser, key, value, expiration, argname):
926 if ':' in key:
927 parser.error('%s key cannot contain ":"' % argname)
928 if key.strip() != key:
929 parser.error('%s key has whitespace' % argname)
930 if not key:
931 parser.error('%s key is empty' % argname)
932
933 if value.strip() != value:
934 parser.error('%s value has whitespace' % argname)
935 if not value:
936 parser.error('%s value is empty' % argname)
937
938 if expiration is not None:
939 try:
940 expiration = int(expiration)
941 except ValueError:
942 parser.error('%s expiration is not an integer' % argname)
943 if expiration <= 0:
944 parser.error('%s expiration should be positive' % argname)
945 if expiration % 60 != 0:
946 parser.error('%s expiration is not divisible by 60' % argname)
947
948
maruelaf6b06c2017-06-08 06:26:53 -0700949def process_filter_options(parser, options):
950 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000951 _validate_filter_option(parser, key, value, None, 'dimension')
952 for key, value, exp in options.optional_dimensions:
953 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700954 options.dimensions.sort()
955
956
Vadim Shtayurab450c602014-05-12 19:23:25 -0700957def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400958 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700959 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700960 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700961 help='Number of shards to trigger and collect.')
962 parser.add_option_group(parser.sharding_group)
963
964
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400965def add_trigger_options(parser):
966 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500967 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400968 add_filter_options(parser)
969
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400970 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700972 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500973 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800974 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500975 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700976 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800977 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800978 '--env-prefix', default=[], action='append', nargs=2,
979 metavar='VAR local/path',
980 help='Prepend task-relative `local/path` to the task\'s VAR environment '
981 'variable using os-appropriate pathsep character. Can be specified '
982 'multiple times for the same VAR to add multiple paths.')
983 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400984 '--idempotent', action='store_true', default=False,
985 help='When set, the server will actively try to find a previous task '
986 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800987 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700988 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700989 help='The optional path to a file containing the secret_bytes to use with'
990 'this task.')
maruel681d6802017-01-17 16:56:03 -0800991 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700992 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400993 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800994 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700995 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400996 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800997 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500998 '--raw-cmd', action='store_true', default=False,
999 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -07001000 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -08001001 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001002 '--relative-cwd',
1003 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1004 'requires --raw-cmd')
1005 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001006 '--cipd-package', action='append', default=[], metavar='PKG',
1007 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -07001008 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001009 group.add_option(
1010 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -07001011 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001012 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1013 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001014 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001015 help='Email of a service account to run the task as, or literal "bot" '
1016 'string to indicate that the task should use the same account the '
1017 'bot itself is using to authenticate to Swarming. Don\'t use task '
1018 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001019 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001020 '--pool-task-template',
1021 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1022 default='AUTO',
1023 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
1024 'By default, the pool\'s TaskTemplate is automatically selected, '
1025 'according the pool configuration on the server. Choices are: '
1026 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
1027 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001028 '-o', '--output', action='append', default=[], metavar='PATH',
1029 help='A list of files to return in addition to those written to '
1030 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1031 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001032 group.add_option(
1033 '--wait-for-capacity', action='store_true', default=False,
1034 help='Instructs to leave the task PENDING even if there\'s no known bot '
1035 'that could run this task, otherwise the task will be denied with '
1036 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001037 parser.add_option_group(group)
1038
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001039 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001040 group.add_option(
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +00001041 '--priority', type='int', default=200,
maruel681d6802017-01-17 16:56:03 -08001042 help='The lower value, the more important the task is')
1043 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001044 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001045 help='Display name of the task. Defaults to '
1046 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1047 'isolated file is provided, if a hash is provided, it defaults to '
1048 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1049 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001050 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001051 help='Tags to assign to the task.')
1052 group.add_option(
1053 '--user', default='',
1054 help='User associated with the task. Defaults to authenticated user on '
1055 'the server.')
1056 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001057 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001058 help='Seconds to allow the task to be pending for a bot to run before '
1059 'this task request expires.')
1060 group.add_option(
1061 '--deadline', type='int', dest='expiration',
1062 help=optparse.SUPPRESS_HELP)
1063 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001064
1065
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001066def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001067 """Processes trigger options and does preparatory steps.
1068
1069 Returns:
1070 NewTaskRequest instance.
1071 """
maruelaf6b06c2017-06-08 06:26:53 -07001072 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001073 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001074 if args and args[0] == '--':
1075 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001077 if not options.dimensions:
1078 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001079 if not any(k == 'pool' for k, _v in options.dimensions):
1080 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001081 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1082 parser.error('--tags must be in the format key:value')
1083 if options.raw_cmd and not args:
1084 parser.error(
1085 'Arguments with --raw-cmd should be passed after -- as command '
1086 'delimiter.')
1087 if options.isolate_server and not options.namespace:
1088 parser.error(
1089 '--namespace must be a valid value when --isolate-server is used')
1090 if not options.isolated and not options.raw_cmd:
1091 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1092
1093 # Isolated
1094 # --isolated is required only if --raw-cmd wasn't provided.
1095 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1096 # preferred server.
1097 isolateserver.process_isolate_server_options(
1098 parser, options, False, not options.raw_cmd)
1099 inputs_ref = None
1100 if options.isolate_server:
1101 inputs_ref = FilesRef(
1102 isolated=options.isolated,
1103 isolatedserver=options.isolate_server,
1104 namespace=options.namespace)
1105
1106 # Command
1107 command = None
1108 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001109 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001110 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001111 if options.relative_cwd:
1112 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1113 if not a.startswith(os.getcwd()):
1114 parser.error(
1115 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001116 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001117 if options.relative_cwd:
1118 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001119 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001120
maruel0a25f6c2017-05-10 10:43:23 -07001121 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001122 cipd_packages = []
1123 for p in options.cipd_package:
1124 split = p.split(':', 2)
1125 if len(split) != 3:
1126 parser.error('CIPD packages must take the form: path:package:version')
1127 cipd_packages.append(CipdPackage(
1128 package_name=split[1],
1129 path=split[0],
1130 version=split[2]))
1131 cipd_input = None
1132 if cipd_packages:
1133 cipd_input = CipdInput(
1134 client_package=None,
1135 packages=cipd_packages,
1136 server=None)
1137
maruel0a25f6c2017-05-10 10:43:23 -07001138 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001139 secret_bytes = None
1140 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001141 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001142 secret_bytes = f.read().encode('base64')
1143
maruel0a25f6c2017-05-10 10:43:23 -07001144 # Named caches
maruel681d6802017-01-17 16:56:03 -08001145 caches = [
1146 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1147 for i in options.named_cache
1148 ]
maruel0a25f6c2017-05-10 10:43:23 -07001149
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001150 env_prefixes = {}
1151 for k, v in options.env_prefix:
1152 env_prefixes.setdefault(k, []).append(v)
1153
Brad Hallf78187a2018-10-19 17:08:55 +00001154 # Get dimensions into the key/value format we can manipulate later.
1155 orig_dims = [
1156 {'key': key, 'value': value} for key, value in options.dimensions]
1157 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1158
1159 # Construct base properties that we will use for all the slices, adding in
1160 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001161 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001162 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001163 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001164 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001165 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001166 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001167 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001168 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001169 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001170 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001171 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001172 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001173 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001174 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001175 outputs=options.output,
1176 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001177
1178 slices = []
1179
1180 # Group the optional dimensions by expiration.
1181 dims_by_exp = {}
1182 for key, value, exp_secs in options.optional_dimensions:
1183 dims_by_exp.setdefault(int(exp_secs), []).append(
1184 {'key': key, 'value': value})
1185
1186 # Create the optional slices with expiration deltas, we fix up the properties
1187 # below.
1188 last_exp = 0
1189 for expiration_secs in sorted(dims_by_exp):
1190 t = TaskSlice(
1191 expiration_secs=expiration_secs - last_exp,
1192 properties=properties,
1193 wait_for_capacity=False)
1194 slices.append(t)
1195 last_exp = expiration_secs
1196
1197 # Add back in the default slice (the last one).
1198 exp = max(int(options.expiration) - last_exp, 60)
1199 base_task_slice = TaskSlice(
1200 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001201 properties=properties,
1202 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001203 slices.append(base_task_slice)
1204
Brad Hall7f463e62018-11-16 16:13:30 +00001205 # Add optional dimensions to the task slices, replacing a dimension that
1206 # has the same key if it is a dimension where repeating isn't valid (otherwise
1207 # we append it). Currently the only dimension we can repeat is "caches"; the
1208 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001209 extra_dims = []
Brad Hall7f463e62018-11-16 16:13:30 +00001210 for i, (_, kvs) in enumerate(sorted(dims_by_exp.iteritems(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001211 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001212 # Replace or append the key/value pairs for this expiration in extra_dims;
1213 # we keep extra_dims around because we are iterating backwards and filling
1214 # in slices with shorter expirations. Dimensions expire as time goes on so
1215 # the slices that expire earlier will generally have more dimensions.
1216 for kv in kvs:
1217 if kv['key'] == 'caches':
1218 extra_dims.append(kv)
1219 else:
1220 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1221 # Then, add all the optional dimensions to the original dimension set, again
1222 # replacing if needed.
1223 for kv in extra_dims:
1224 if kv['key'] == 'caches':
1225 dims.append(kv)
1226 else:
1227 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001228 dims.sort(key=lambda x: (x['key'], x['value']))
1229 slice_properties = properties._replace(dimensions=dims)
1230 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1231
maruel77f720b2015-09-15 12:35:22 -07001232 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001233 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001234 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001235 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001236 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001237 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001238 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001239 user=options.user,
1240 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001241
1242
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001243class TaskOutputStdoutOption(optparse.Option):
1244 """Where to output the each task's console output (stderr/stdout).
1245
1246 The output will be;
1247 none - not be downloaded.
1248 json - stored in summary.json file *only*.
1249 console - shown on stdout *only*.
1250 all - stored in summary.json and shown on stdout.
1251 """
1252
1253 choices = ['all', 'json', 'console', 'none']
1254
1255 def __init__(self, *args, **kw):
1256 optparse.Option.__init__(
1257 self,
1258 *args,
1259 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001260 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001261 help=re.sub('\s\s*', ' ', self.__doc__),
1262 **kw)
1263
1264 def convert_value(self, opt, value):
1265 if value not in self.choices:
1266 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1267 self.get_opt_string(), self.choices, value))
1268 stdout_to = []
1269 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001270 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001271 elif value != 'none':
1272 stdout_to = [value]
1273 return stdout_to
1274
1275
maruel@chromium.org0437a732013-08-27 16:05:52 +00001276def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001277 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001278 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001279 help='Timeout to wait for result, set to -1 for no timeout and get '
1280 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001281 parser.group_logging.add_option(
1282 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001283 parser.group_logging.add_option(
1284 '--print-status-updates', action='store_true',
1285 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001286 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001287 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001288 '--task-summary-json',
1289 metavar='FILE',
1290 help='Dump a summary of task results to this file as json. It contains '
1291 'only shards statuses as know to server directly. Any output files '
1292 'emitted by the task can be collected by using --task-output-dir')
1293 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001294 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001295 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001296 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001297 'directory contains per-shard directory with output files produced '
1298 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001299 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001300 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001301 parser.task_output_group.add_option(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001302 '--filepath-filter',
1303 help='This is regexp filter used to specify downloaded filepath when '
1304 'collecting isolated output.')
1305 parser.task_output_group.add_option(
maruel9531ce02016-04-13 06:11:23 -07001306 '--perf', action='store_true', default=False,
1307 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001308 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001309
1310
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001311def process_collect_options(parser, options):
1312 # Only negative -1 is allowed, disallow other negative values.
1313 if options.timeout != -1 and options.timeout < 0:
1314 parser.error('Invalid --timeout value')
1315
1316
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001317@subcommand.usage('bots...')
1318def CMDbot_delete(parser, args):
1319 """Forcibly deletes bots from the Swarming server."""
1320 parser.add_option(
1321 '-f', '--force', action='store_true',
1322 help='Do not prompt for confirmation')
1323 options, args = parser.parse_args(args)
1324 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001325 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001326
1327 bots = sorted(args)
1328 if not options.force:
1329 print('Delete the following bots?')
1330 for bot in bots:
1331 print(' %s' % bot)
1332 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1333 print('Goodbye.')
1334 return 1
1335
1336 result = 0
1337 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001338 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001339 if net.url_read_json(url, data={}, method='POST') is None:
1340 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001341 result = 1
1342 return result
1343
1344
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001345def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001346 """Returns information about the bots connected to the Swarming server."""
1347 add_filter_options(parser)
1348 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001349 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001350 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001351 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001352 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001353 help='Keep both dead and alive bots')
1354 parser.filter_group.add_option(
1355 '--busy', action='store_true', help='Keep only busy bots')
1356 parser.filter_group.add_option(
1357 '--idle', action='store_true', help='Keep only idle bots')
1358 parser.filter_group.add_option(
1359 '--mp', action='store_true',
1360 help='Keep only Machine Provider managed bots')
1361 parser.filter_group.add_option(
1362 '--non-mp', action='store_true',
1363 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001364 parser.filter_group.add_option(
1365 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001366 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001367 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001368 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001369
1370 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001371 parser.error('Use only one of --keep-dead or --dead-only')
1372 if options.busy and options.idle:
1373 parser.error('Use only one of --busy or --idle')
1374 if options.mp and options.non_mp:
1375 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001376
smut281c3902018-05-30 17:50:05 -07001377 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001378 values = []
1379 if options.dead_only:
1380 values.append(('is_dead', 'TRUE'))
1381 elif options.keep_dead:
1382 values.append(('is_dead', 'NONE'))
1383 else:
1384 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001385
maruelaf6b06c2017-06-08 06:26:53 -07001386 if options.busy:
1387 values.append(('is_busy', 'TRUE'))
1388 elif options.idle:
1389 values.append(('is_busy', 'FALSE'))
1390 else:
1391 values.append(('is_busy', 'NONE'))
1392
1393 if options.mp:
1394 values.append(('is_mp', 'TRUE'))
1395 elif options.non_mp:
1396 values.append(('is_mp', 'FALSE'))
1397 else:
1398 values.append(('is_mp', 'NONE'))
1399
1400 for key, value in options.dimensions:
1401 values.append(('dimensions', '%s:%s' % (key, value)))
1402 url += urllib.urlencode(values)
1403 try:
1404 data, yielder = get_yielder(url, 0)
1405 bots = data.get('items') or []
1406 for items in yielder():
1407 if items:
1408 bots.extend(items)
1409 except Failure as e:
1410 sys.stderr.write('\n%s\n' % e)
1411 return 1
maruel77f720b2015-09-15 12:35:22 -07001412 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001413 print bot['bot_id']
1414 if not options.bare:
1415 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1416 print ' %s' % json.dumps(dimensions, sort_keys=True)
1417 if bot.get('task_id'):
1418 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001419 return 0
1420
1421
maruelfd0a90c2016-06-10 11:51:10 -07001422@subcommand.usage('task_id')
1423def CMDcancel(parser, args):
1424 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001425 parser.add_option(
1426 '-k', '--kill-running', action='store_true', default=False,
1427 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001428 options, args = parser.parse_args(args)
1429 if not args:
1430 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001431 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001432 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001433 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001434 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001435 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001436 print('Deleting %s failed. Probably already gone' % task_id)
1437 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001438 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001439 return 0
1440
1441
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001442@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001443def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001444 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001445
1446 The result can be in multiple part if the execution was sharded. It can
1447 potentially have retries.
1448 """
1449 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001450 parser.add_option(
1451 '-j', '--json',
1452 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001453 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001454 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001455 if not args and not options.json:
1456 parser.error('Must specify at least one task id or --json.')
1457 if args and options.json:
1458 parser.error('Only use one of task id or --json.')
1459
1460 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001461 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001462 try:
maruel1ceb3872015-10-14 06:10:44 -07001463 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001464 data = json.load(f)
1465 except (IOError, ValueError):
1466 parser.error('Failed to open %s' % options.json)
1467 try:
1468 tasks = sorted(
1469 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1470 args = [t['task_id'] for t in tasks]
1471 except (KeyError, TypeError):
1472 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001473 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001474 # Take in account all the task slices.
1475 offset = 0
1476 for s in data['request']['task_slices']:
1477 m = (offset + s['properties']['execution_timeout_secs'] +
1478 s['expiration_secs'])
1479 if m > options.timeout:
1480 options.timeout = m
1481 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001482 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001483 else:
1484 valid = frozenset('0123456789abcdef')
1485 if any(not valid.issuperset(task_id) for task_id in args):
1486 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001487
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001488 try:
1489 return collect(
1490 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001491 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001492 options.timeout,
1493 options.decorate,
1494 options.print_status_updates,
1495 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001496 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001497 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001498 options.perf,
1499 options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001500 except Failure:
1501 on_error.report(None)
1502 return 1
1503
1504
maruel77f720b2015-09-15 12:35:22 -07001505@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001506def CMDpost(parser, args):
1507 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1508
1509 Input data must be sent to stdin, result is printed to stdout.
1510
1511 If HTTP response code >= 400, returns non-zero.
1512 """
1513 options, args = parser.parse_args(args)
1514 if len(args) != 1:
1515 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001516 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001517 data = sys.stdin.read()
1518 try:
1519 resp = net.url_read(url, data=data, method='POST')
1520 except net.TimeoutError:
1521 sys.stderr.write('Timeout!\n')
1522 return 1
1523 if not resp:
1524 sys.stderr.write('No response!\n')
1525 return 1
1526 sys.stdout.write(resp)
1527 return 0
1528
1529
1530@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001531def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001532 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1533 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001534
1535 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001536 Raw task request and results:
1537 swarming.py query -S server-url.com task/123456/request
1538 swarming.py query -S server-url.com task/123456/result
1539
maruel77f720b2015-09-15 12:35:22 -07001540 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001541 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001542
maruelaf6b06c2017-06-08 06:26:53 -07001543 Listing last 10 tasks on a specific bot named 'bot1':
1544 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001545
maruelaf6b06c2017-06-08 06:26:53 -07001546 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001547 quoting is important!:
1548 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001549 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001550 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001551 parser.add_option(
1552 '-L', '--limit', type='int', default=200,
1553 help='Limit to enforce on limitless items (like number of tasks); '
1554 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001555 parser.add_option(
1556 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001557 parser.add_option(
1558 '--progress', action='store_true',
1559 help='Prints a dot at each request to show progress')
1560 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001561 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001562 parser.error(
1563 'Must specify only method name and optionally query args properly '
1564 'escaped.')
smut281c3902018-05-30 17:50:05 -07001565 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001566 try:
1567 data, yielder = get_yielder(base_url, options.limit)
1568 for items in yielder():
1569 if items:
1570 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001571 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001572 sys.stderr.write('.')
1573 sys.stderr.flush()
1574 except Failure as e:
1575 sys.stderr.write('\n%s\n' % e)
1576 return 1
maruel77f720b2015-09-15 12:35:22 -07001577 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001578 sys.stderr.write('\n')
1579 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001580 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001581 options.json = unicode(os.path.abspath(options.json))
1582 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001583 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001584 try:
maruel77f720b2015-09-15 12:35:22 -07001585 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001586 sys.stdout.write('\n')
1587 except IOError:
1588 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001589 return 0
1590
1591
maruel77f720b2015-09-15 12:35:22 -07001592def CMDquery_list(parser, args):
1593 """Returns list of all the Swarming APIs that can be used with command
1594 'query'.
1595 """
1596 parser.add_option(
1597 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1598 options, args = parser.parse_args(args)
1599 if args:
1600 parser.error('No argument allowed.')
1601
1602 try:
1603 apis = endpoints_api_discovery_apis(options.swarming)
1604 except APIError as e:
1605 parser.error(str(e))
1606 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001607 options.json = unicode(os.path.abspath(options.json))
1608 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001609 json.dump(apis, f)
1610 else:
1611 help_url = (
1612 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1613 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001614 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1615 if i:
1616 print('')
maruel77f720b2015-09-15 12:35:22 -07001617 print api_id
maruel11e31af2017-02-15 07:30:50 -08001618 print ' ' + api['description'].strip()
1619 if 'resources' in api:
1620 # Old.
1621 for j, (resource_name, resource) in enumerate(
1622 sorted(api['resources'].iteritems())):
1623 if j:
1624 print('')
1625 for method_name, method in sorted(resource['methods'].iteritems()):
1626 # Only list the GET ones.
1627 if method['httpMethod'] != 'GET':
1628 continue
1629 print '- %s.%s: %s' % (
1630 resource_name, method_name, method['path'])
1631 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001632 ' ' + l for l in textwrap.wrap(
1633 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001634 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1635 else:
1636 # New.
1637 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001638 # Only list the GET ones.
1639 if method['httpMethod'] != 'GET':
1640 continue
maruel11e31af2017-02-15 07:30:50 -08001641 print '- %s: %s' % (method['id'], method['path'])
1642 print('\n'.join(
1643 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001644 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1645 return 0
1646
1647
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001648@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001649def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001650 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001651
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001652 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001653 """
1654 add_trigger_options(parser)
1655 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001656 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001657 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001658 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001659 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001660 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001661 tasks = trigger_task_shards(
1662 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001663 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001664 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001665 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001666 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001667 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001668 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001669 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001670 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001671 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001672 task_ids = [
1673 t['task_id']
1674 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1675 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001676 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001677 offset = 0
1678 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001679 m = (offset + s.properties.execution_timeout_secs +
1680 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001681 if m > options.timeout:
1682 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001683 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001684 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001685 try:
1686 return collect(
1687 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001688 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001689 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001690 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001691 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001692 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001693 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001694 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001695 options.perf,
1696 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001697 except Failure:
1698 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001699 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001700
1701
maruel18122c62015-10-23 06:31:23 -07001702@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001703def CMDreproduce(parser, args):
1704 """Runs a task locally that was triggered on the server.
1705
1706 This running locally the same commands that have been run on the bot. The data
1707 downloaded will be in a subdirectory named 'work' of the current working
1708 directory.
maruel18122c62015-10-23 06:31:23 -07001709
1710 You can pass further additional arguments to the target command by passing
1711 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001712 """
maruelc070e672016-02-22 17:32:57 -08001713 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001714 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001715 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001716 parser.add_option(
1717 '--work', metavar='DIR', default='work',
1718 help='Directory to map the task input files into')
1719 parser.add_option(
1720 '--cache', metavar='DIR', default='cache',
1721 help='Directory that contains the input cache')
1722 parser.add_option(
1723 '--leak', action='store_true',
1724 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001725 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001726 extra_args = []
1727 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001728 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001729 if len(args) > 1:
1730 if args[1] == '--':
1731 if len(args) > 2:
1732 extra_args = args[2:]
1733 else:
1734 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001735
smut281c3902018-05-30 17:50:05 -07001736 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001737 request = net.url_read_json(url)
1738 if not request:
1739 print >> sys.stderr, 'Failed to retrieve request data for the task'
1740 return 1
1741
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001742 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001743 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001744 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001745 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001746 cachedir = unicode(os.path.abspath('cipd_cache'))
1747 if not fs.exists(cachedir):
1748 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001749
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001750 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001751 env = os.environ.copy()
1752 env['SWARMING_BOT_ID'] = 'reproduce'
1753 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001754 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001755 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001756 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001757 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001758 if not i['value']:
1759 env.pop(key, None)
1760 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001761 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001762
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001763 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001764 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001765 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001766 for i in env_prefixes:
1767 key = i['key']
1768 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001769 cur = env.get(key)
1770 if cur:
1771 paths.append(cur)
1772 env[key] = os.path.pathsep.join(paths)
1773
iannucci31ab9192017-05-02 19:11:56 -07001774 command = []
nodir152cba62016-05-12 16:08:56 -07001775 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001776 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001777 server_ref = isolate_storage.ServerRef(
maruel29ab2fd2015-10-16 11:44:01 -07001778 properties['inputs_ref']['isolatedserver'],
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001779 properties['inputs_ref']['namespace'])
1780 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001781 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1782 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1783 # leak.
1784 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001785 cache = local_caching.DiskContentAddressedCache(
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001786 unicode(os.path.abspath(options.cache)), policies,
1787 server_ref.hash_algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001788 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001789 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001790 command = bundle.command
1791 if bundle.relative_cwd:
1792 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001793 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001794
1795 if properties.get('command'):
1796 command.extend(properties['command'])
1797
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001798 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001799 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001800 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001801 new_command = run_isolated.process_command(command, 'invalid', None)
1802 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001803 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001804 else:
1805 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001806 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001807 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001808 command, options.output, None)
1809 if not os.path.isdir(options.output):
1810 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001811 command = new_command
1812 file_path.ensure_command_has_abs_path(command, workdir)
1813
1814 if properties.get('cipd_input'):
1815 ci = properties['cipd_input']
1816 cp = ci['client_package']
1817 client_manager = cipd.get_client(
1818 ci['server'], cp['package_name'], cp['version'], cachedir)
1819
1820 with client_manager as client:
1821 by_path = collections.defaultdict(list)
1822 for pkg in ci['packages']:
1823 path = pkg['path']
1824 # cipd deals with 'root' as ''
1825 if path == '.':
1826 path = ''
1827 by_path[path].append((pkg['package_name'], pkg['version']))
1828 client.ensure(workdir, by_path, cache_dir=cachedir)
1829
maruel77f720b2015-09-15 12:35:22 -07001830 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001831 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001832 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001833 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001834 print >> sys.stderr, str(e)
1835 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001836 finally:
1837 # Do not delete options.cache.
1838 if not options.leak:
1839 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001840
1841
maruel0eb1d1b2015-10-02 14:48:21 -07001842@subcommand.usage('bot_id')
1843def CMDterminate(parser, args):
1844 """Tells a bot to gracefully shut itself down as soon as it can.
1845
1846 This is done by completing whatever current task there is then exiting the bot
1847 process.
1848 """
1849 parser.add_option(
1850 '--wait', action='store_true', help='Wait for the bot to terminate')
1851 options, args = parser.parse_args(args)
1852 if len(args) != 1:
1853 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001854 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001855 request = net.url_read_json(url, data={})
1856 if not request:
1857 print >> sys.stderr, 'Failed to ask for termination'
1858 return 1
1859 if options.wait:
1860 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001861 options.swarming,
1862 [request['task_id']],
1863 0.,
1864 False,
1865 False,
1866 None,
1867 None,
1868 [],
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001869 False,
1870 None)
maruelbfc5f872017-06-10 16:43:17 -07001871 else:
1872 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001873 return 0
1874
1875
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001876@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001877def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001878 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001879
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001880 Passes all extra arguments provided after '--' as additional command line
1881 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001882 """
1883 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001884 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001885 parser.add_option(
1886 '--dump-json',
1887 metavar='FILE',
1888 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001889 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001890 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001891 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001892 tasks = trigger_task_shards(
1893 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001894 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001895 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001896 tasks_sorted = sorted(
1897 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001898 if options.dump_json:
1899 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001900 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001901 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001902 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001903 }
maruel46b015f2015-10-13 18:40:35 -07001904 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001905 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001906 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001907 (options.swarming, options.dump_json))
1908 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001909 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001910 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001911 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1912 print('Or visit:')
1913 for t in tasks_sorted:
1914 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001915 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001916 except Failure:
1917 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001918 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001919
1920
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001921class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001922 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001923 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001924 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001925 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001926 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001927 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001928 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001929 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001930 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001931 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001932
1933 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001934 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001935 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001936 auth.process_auth_options(self, options)
1937 user = self._process_swarming(options)
1938 if hasattr(options, 'user') and not options.user:
1939 options.user = user
1940 return options, args
1941
1942 def _process_swarming(self, options):
1943 """Processes the --swarming option and aborts if not specified.
1944
1945 Returns the identity as determined by the server.
1946 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001947 if not options.swarming:
1948 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001949 try:
1950 options.swarming = net.fix_url(options.swarming)
1951 except ValueError as e:
1952 self.error('--swarming %s' % e)
1953 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001954 try:
1955 user = auth.ensure_logged_in(options.swarming)
1956 except ValueError as e:
1957 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001958 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001959
1960
1961def main(args):
1962 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001963 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001964
1965
1966if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001967 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001968 fix_encoding.fix_encoding()
1969 tools.disable_buffering()
1970 colorama.init()
1971 sys.exit(main(sys.argv[1:]))