blob: 1b565e2167756ba7c49eaa42c5841b5dfff32f6f [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
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -04008__version__ = '0.12'
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 Ruel2666d9c2018-05-18 13:52:02 -040040import local_caching
maruelc070e672016-02-22 17:32:57 -080041import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000042
43
tansella4949442016-06-23 22:34:32 -070044ROOT_DIR = os.path.dirname(os.path.abspath(
45 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050046
47
48class Failure(Exception):
49 """Generic failure."""
50 pass
51
52
maruel0a25f6c2017-05-10 10:43:23 -070053def default_task_name(options):
54 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050055 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070056 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070057 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070058 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070059 if options.isolated:
60 task_name += u'/' + options.isolated
61 return task_name
62 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050063
64
65### Triggering.
66
67
maruel77f720b2015-09-15 12:35:22 -070068# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070069CipdPackage = collections.namedtuple(
70 'CipdPackage',
71 [
72 'package_name',
73 'path',
74 'version',
75 ])
76
77
78# See ../appengine/swarming/swarming_rpcs.py.
79CipdInput = collections.namedtuple(
80 'CipdInput',
81 [
82 'client_package',
83 'packages',
84 'server',
85 ])
86
87
88# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070089FilesRef = collections.namedtuple(
90 'FilesRef',
91 [
92 'isolated',
93 'isolatedserver',
94 'namespace',
95 ])
96
97
98# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080099StringListPair = collections.namedtuple(
100 'StringListPair', [
101 'key',
102 'value', # repeated string
103 ]
104)
105
106
107# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700108TaskProperties = collections.namedtuple(
109 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110 [
maruel681d6802017-01-17 16:56:03 -0800111 'caches',
borenet02f772b2016-06-22 12:42:19 -0700112 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500113 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500114 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500115 'dimensions',
116 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800117 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700118 'execution_timeout_secs',
119 'extra_args',
120 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500121 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700122 'inputs_ref',
123 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700124 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700125 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700126 ])
127
128
129# See ../appengine/swarming/swarming_rpcs.py.
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400130TaskSlice = collections.namedtuple(
131 'TaskSlice',
132 [
133 'expiration_secs',
134 'properties',
135 'wait_for_capacity',
136 ])
137
138
139# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700140NewTaskRequest = collections.namedtuple(
141 'NewTaskRequest',
142 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500143 'name',
maruel77f720b2015-09-15 12:35:22 -0700144 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500145 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400146 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700147 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500148 'tags',
149 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500150 ])
151
152
maruel77f720b2015-09-15 12:35:22 -0700153def namedtuple_to_dict(value):
154 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400155 if hasattr(value, '_asdict'):
156 return namedtuple_to_dict(value._asdict())
157 if isinstance(value, (list, tuple)):
158 return [namedtuple_to_dict(v) for v in value]
159 if isinstance(value, dict):
160 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
161 return value
maruel77f720b2015-09-15 12:35:22 -0700162
163
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700164def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800165 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700166
167 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500168 """
maruel77f720b2015-09-15 12:35:22 -0700169 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700170 # Don't send 'service_account' if it is None to avoid confusing older
171 # version of the server that doesn't know about 'service_account' and don't
172 # use it at all.
173 if not out['service_account']:
174 out.pop('service_account')
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400175 out['task_slices'][0]['properties']['dimensions'] = [
maruel77f720b2015-09-15 12:35:22 -0700176 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400177 for k, v in out['task_slices'][0]['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700178 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400179 out['task_slices'][0]['properties']['env'] = [
maruel77f720b2015-09-15 12:35:22 -0700180 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400181 for k, v in out['task_slices'][0]['properties']['env'].iteritems()
maruel77f720b2015-09-15 12:35:22 -0700182 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400183 out['task_slices'][0]['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700184 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500185
186
maruel77f720b2015-09-15 12:35:22 -0700187def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500188 """Triggers a request on the Swarming server and returns the json data.
189
190 It's the low-level function.
191
192 Returns:
193 {
194 'request': {
195 'created_ts': u'2010-01-02 03:04:05',
196 'name': ..
197 },
198 'task_id': '12300',
199 }
200 """
201 logging.info('Triggering: %s', raw_request['name'])
202
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500203 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700204 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500205 if not result:
206 on_error.report('Failed to trigger task %s' % raw_request['name'])
207 return None
maruele557bce2015-11-17 09:01:27 -0800208 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800209 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800210 msg = 'Failed to trigger task %s' % raw_request['name']
211 if result['error'].get('errors'):
212 for err in result['error']['errors']:
213 if err.get('message'):
214 msg += '\nMessage: %s' % err['message']
215 if err.get('debugInfo'):
216 msg += '\nDebug info:\n%s' % err['debugInfo']
217 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800218 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800219
220 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800221 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500222 return result
223
224
225def setup_googletest(env, shards, index):
226 """Sets googletest specific environment variables."""
227 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700228 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
229 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
230 env = env[:]
231 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
232 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500233 return env
234
235
236def trigger_task_shards(swarming, task_request, shards):
237 """Triggers one or many subtasks of a sharded task.
238
239 Returns:
240 Dict with task details, returned to caller as part of --dump-json output.
241 None in case of failure.
242 """
243 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700244 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500245 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400246 req['task_slices'][0]['properties']['env'] = setup_googletest(
247 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700248 req['name'] += ':%s:%s' % (index, shards)
249 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250
251 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500252 tasks = {}
253 priority_warning = False
254 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700255 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500256 if not task:
257 break
258 logging.info('Request result: %s', task)
259 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400260 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500261 priority_warning = True
262 print >> sys.stderr, (
263 'Priority was reset to %s' % task['request']['priority'])
264 tasks[request['name']] = {
265 'shard_index': index,
266 'task_id': task['task_id'],
267 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
268 }
269
270 # Some shards weren't triggered. Abort everything.
271 if len(tasks) != len(requests):
272 if tasks:
273 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
274 len(tasks), len(requests))
275 for task_dict in tasks.itervalues():
276 abort_task(swarming, task_dict['task_id'])
277 return None
278
279 return tasks
280
281
282### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000283
284
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700285# How often to print status updates to stdout in 'collect'.
286STATUS_UPDATE_INTERVAL = 15 * 60.
287
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400288
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400289class State(object):
290 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000291
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400292 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
293 values are part of the API so if they change, the API changed.
294
295 It's in fact an enum. Values should be in decreasing order of importance.
296 """
297 RUNNING = 0x10
298 PENDING = 0x20
299 EXPIRED = 0x30
300 TIMED_OUT = 0x40
301 BOT_DIED = 0x50
302 CANCELED = 0x60
303 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400304 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400305 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400306
maruel77f720b2015-09-15 12:35:22 -0700307 STATES = (
308 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400309 'COMPLETED', 'KILLED', 'NO_RESOURCE')
maruel77f720b2015-09-15 12:35:22 -0700310 STATES_RUNNING = ('RUNNING', 'PENDING')
311 STATES_NOT_RUNNING = (
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400312 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED', 'KILLED',
313 'NO_RESOURCE')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400314 STATES_DONE = ('TIMED_OUT', 'COMPLETED', 'KILLED')
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400315 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED', 'NO_RESOURCE')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400316
317 _NAMES = {
318 RUNNING: 'Running',
319 PENDING: 'Pending',
320 EXPIRED: 'Expired',
321 TIMED_OUT: 'Execution timed out',
322 BOT_DIED: 'Bot died',
323 CANCELED: 'User canceled',
324 COMPLETED: 'Completed',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400325 KILLED: 'User killed',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400326 NO_RESOURCE: 'No resource',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400327 }
328
maruel77f720b2015-09-15 12:35:22 -0700329 _ENUMS = {
330 'RUNNING': RUNNING,
331 'PENDING': PENDING,
332 'EXPIRED': EXPIRED,
333 'TIMED_OUT': TIMED_OUT,
334 'BOT_DIED': BOT_DIED,
335 'CANCELED': CANCELED,
336 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400337 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400338 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700339 }
340
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400341 @classmethod
342 def to_string(cls, state):
343 """Returns a user-readable string representing a State."""
344 if state not in cls._NAMES:
345 raise ValueError('Invalid state %s' % state)
346 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000347
maruel77f720b2015-09-15 12:35:22 -0700348 @classmethod
349 def from_enum(cls, state):
350 """Returns int value based on the string."""
351 if state not in cls._ENUMS:
352 raise ValueError('Invalid state %s' % state)
353 return cls._ENUMS[state]
354
maruel@chromium.org0437a732013-08-27 16:05:52 +0000355
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700356class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700357 """Assembles task execution summary (for --task-summary-json output).
358
359 Optionally fetches task outputs from isolate server to local disk (used when
360 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361
362 This object is shared among multiple threads running 'retrieve_results'
363 function, in particular they call 'process_shard_result' method in parallel.
364 """
365
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000366 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700367 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
368
369 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700370 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700371 shard_count: expected number of task shards.
372 """
maruel12e30012015-10-09 11:55:35 -0700373 self.task_output_dir = (
374 unicode(os.path.abspath(task_output_dir))
375 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000376 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377 self.shard_count = shard_count
378
379 self._lock = threading.Lock()
380 self._per_shard_results = {}
381 self._storage = None
382
nodire5028a92016-04-29 14:38:21 -0700383 if self.task_output_dir:
384 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385
Vadim Shtayurab450c602014-05-12 19:23:25 -0700386 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 """Stores results of a single task shard, fetches output files if necessary.
388
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400389 Modifies |result| in place.
390
maruel77f720b2015-09-15 12:35:22 -0700391 shard_index is 0-based.
392
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700393 Called concurrently from multiple threads.
394 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700395 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700396 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700397 if shard_index < 0 or shard_index >= self.shard_count:
398 logging.warning(
399 'Shard index %d is outside of expected range: [0; %d]',
400 shard_index, self.shard_count - 1)
401 return
402
maruel77f720b2015-09-15 12:35:22 -0700403 if result.get('outputs_ref'):
404 ref = result['outputs_ref']
405 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
406 ref['isolatedserver'],
407 urllib.urlencode(
408 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400409
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700410 # Store result dict of that shard, ignore results we've already seen.
411 with self._lock:
412 if shard_index in self._per_shard_results:
413 logging.warning('Ignoring duplicate shard index %d', shard_index)
414 return
415 self._per_shard_results[shard_index] = result
416
417 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700418 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400419 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700420 result['outputs_ref']['isolatedserver'],
421 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400422 if storage:
423 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400424 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
425 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400426 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700427 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400428 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400429 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700430 os.path.join(self.task_output_dir, str(shard_index)),
431 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700432
433 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700434 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700435 with self._lock:
436 # Write an array of shard results with None for missing shards.
437 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438 'shards': [
439 self._per_shard_results.get(i) for i in xrange(self.shard_count)
440 ],
441 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000442
443 # Don't store stdout in the summary if not requested too.
444 if "json" not in self.task_output_stdout:
445 for shard_json in summary['shards']:
446 if not shard_json:
447 continue
448 if "output" in shard_json:
449 del shard_json["output"]
450 if "outputs" in shard_json:
451 del shard_json["outputs"]
452
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700453 # Write summary.json to task_output_dir as well.
454 if self.task_output_dir:
455 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700456 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700457 summary,
458 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700459 if self._storage:
460 self._storage.close()
461 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700462 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700463
464 def _get_storage(self, isolate_server, namespace):
465 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700466 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700467 with self._lock:
468 if not self._storage:
469 self._storage = isolateserver.get_storage(isolate_server, namespace)
470 else:
471 # Shards must all use exact same isolate server and namespace.
472 if self._storage.location != isolate_server:
473 logging.error(
474 'Task shards are using multiple isolate servers: %s and %s',
475 self._storage.location, isolate_server)
476 return None
477 if self._storage.namespace != namespace:
478 logging.error(
479 'Task shards are using multiple namespaces: %s and %s',
480 self._storage.namespace, namespace)
481 return None
482 return self._storage
483
484
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500485def now():
486 """Exists so it can be mocked easily."""
487 return time.time()
488
489
maruel77f720b2015-09-15 12:35:22 -0700490def parse_time(value):
491 """Converts serialized time from the API to datetime.datetime."""
492 # When microseconds are 0, the '.123456' suffix is elided. This means the
493 # serialized format is not consistent, which confuses the hell out of python.
494 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
495 try:
496 return datetime.datetime.strptime(value, fmt)
497 except ValueError:
498 pass
499 raise ValueError('Failed to parse %s' % value)
500
501
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700502def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700503 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000504 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400505 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700506
Vadim Shtayurab450c602014-05-12 19:23:25 -0700507 Returns:
508 <result dict> on success.
509 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700510 """
maruel71c61c82016-02-22 06:52:05 -0800511 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700512 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700513 if include_perf:
514 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700515 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700516 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400517 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700518 attempt = 0
519
520 while not should_stop.is_set():
521 attempt += 1
522
523 # Waiting for too long -> give up.
524 current_time = now()
525 if deadline and current_time >= deadline:
526 logging.error('retrieve_results(%s) timed out on attempt %d',
527 base_url, attempt)
528 return None
529
530 # Do not spin too fast. Spin faster at the beginning though.
531 # Start with 1 sec delay and for each 30 sec of waiting add another second
532 # of delay, until hitting 15 sec ceiling.
533 if attempt > 1:
534 max_delay = min(15, 1 + (current_time - started) / 30.0)
535 delay = min(max_delay, deadline - current_time) if deadline else max_delay
536 if delay > 0:
537 logging.debug('Waiting %.1f sec before retrying', delay)
538 should_stop.wait(delay)
539 if should_stop.is_set():
540 return None
541
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400542 # Disable internal retries in net.url_read_json, since we are doing retries
543 # ourselves.
544 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700545 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
546 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400547 # Retry on 500s only if no timeout is specified.
548 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400549 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400550 if timeout == -1:
551 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400552 continue
maruel77f720b2015-09-15 12:35:22 -0700553
maruelbf53e042015-12-01 15:00:51 -0800554 if result.get('error'):
555 # An error occurred.
556 if result['error'].get('errors'):
557 for err in result['error']['errors']:
558 logging.warning(
559 'Error while reading task: %s; %s',
560 err.get('message'), err.get('debugInfo'))
561 elif result['error'].get('message'):
562 logging.warning(
563 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400564 if timeout == -1:
565 return result
maruelbf53e042015-12-01 15:00:51 -0800566 continue
567
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400568 # When timeout == -1, always return on first attempt. 500s are already
569 # retried in this case.
570 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000571 if fetch_stdout:
572 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700573 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700574 # Record the result, try to fetch attached output files (if any).
575 if output_collector:
576 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700577 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700578 if result.get('internal_failure'):
579 logging.error('Internal error!')
580 elif result['state'] == 'BOT_DIED':
581 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700582 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000583
584
maruel77f720b2015-09-15 12:35:22 -0700585def convert_to_old_format(result):
586 """Converts the task result data from Endpoints API format to old API format
587 for compatibility.
588
589 This goes into the file generated as --task-summary-json.
590 """
591 # Sets default.
592 result.setdefault('abandoned_ts', None)
593 result.setdefault('bot_id', None)
594 result.setdefault('bot_version', None)
595 result.setdefault('children_task_ids', [])
596 result.setdefault('completed_ts', None)
597 result.setdefault('cost_saved_usd', None)
598 result.setdefault('costs_usd', None)
599 result.setdefault('deduped_from', None)
600 result.setdefault('name', None)
601 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700602 result.setdefault('server_versions', None)
603 result.setdefault('started_ts', None)
604 result.setdefault('tags', None)
605 result.setdefault('user', None)
606
607 # Convertion back to old API.
608 duration = result.pop('duration', None)
609 result['durations'] = [duration] if duration else []
610 exit_code = result.pop('exit_code', None)
611 result['exit_codes'] = [int(exit_code)] if exit_code else []
612 result['id'] = result.pop('task_id')
613 result['isolated_out'] = result.get('outputs_ref', None)
614 output = result.pop('output', None)
615 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700616 # server_version
617 # Endpoints result 'state' as string. For compatibility with old code, convert
618 # to int.
619 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700620 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700621 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700622 if 'bot_dimensions' in result:
623 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700624 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700625 }
626 else:
627 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700628
629
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700630def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400631 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000632 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500633 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000634
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700635 Duplicate shards are ignored. Shards are yielded in order of completion.
636 Timed out shards are NOT yielded at all. Caller can compare number of yielded
637 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000638
639 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500640 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 +0000641 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500642
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700643 output_collector is an optional instance of TaskOutputCollector that will be
644 used to fetch files produced by a task from isolate server to the local disk.
645
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500646 Yields:
647 (index, result). In particular, 'result' is defined as the
648 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000649 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400651 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700652 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700653 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700654
maruel@chromium.org0437a732013-08-27 16:05:52 +0000655 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
656 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700657 # Adds a task to the thread pool to call 'retrieve_results' and return
658 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400659 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700660 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000661 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400662 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000663 task_id, timeout, should_stop, output_collector, include_perf,
664 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700665
666 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400667 for shard_index, task_id in enumerate(task_ids):
668 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700669
670 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400671 shards_remaining = range(len(task_ids))
672 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700673 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700674 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700675 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700676 shard_index, result = results_channel.pull(
677 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 except threading_utils.TaskChannel.Timeout:
679 if print_status_updates:
680 print(
681 'Waiting for results from the following shards: %s' %
682 ', '.join(map(str, shards_remaining)))
683 sys.stdout.flush()
684 continue
685 except Exception:
686 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700687
688 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700689 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500691 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000692 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700693
Vadim Shtayurab450c602014-05-12 19:23:25 -0700694 # Yield back results to the caller.
695 assert shard_index in shards_remaining
696 shards_remaining.remove(shard_index)
697 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700698
maruel@chromium.org0437a732013-08-27 16:05:52 +0000699 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700700 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000701 should_stop.set()
702
703
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000704def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000705 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700706 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400707 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700708 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
709 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400710 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
711 metadata.get('abandoned_ts')):
712 pending = '%.1fs' % (
713 parse_time(metadata['abandoned_ts']) -
714 parse_time(metadata['created_ts'])
715 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400716 else:
717 pending = 'N/A'
718
maruel77f720b2015-09-15 12:35:22 -0700719 if metadata.get('duration') is not None:
720 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400721 else:
722 duration = 'N/A'
723
maruel77f720b2015-09-15 12:35:22 -0700724 if metadata.get('exit_code') is not None:
725 # Integers are encoded as string to not loose precision.
726 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400727 else:
728 exit_code = 'N/A'
729
730 bot_id = metadata.get('bot_id') or 'N/A'
731
maruel77f720b2015-09-15 12:35:22 -0700732 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400733 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000734 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400735 if metadata.get('state') == 'CANCELED':
736 tag_footer2 = ' Pending: %s CANCELED' % pending
737 elif metadata.get('state') == 'EXPIRED':
738 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400739 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400740 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
741 pending, duration, bot_id, exit_code, metadata['state'])
742 else:
743 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
744 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400745
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000746 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
747 dash_pad = '+-%s-+' % ('-' * tag_len)
748 tag_header = '| %s |' % tag_header.ljust(tag_len)
749 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
750 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400751
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000752 if include_stdout:
753 return '\n'.join([
754 dash_pad,
755 tag_header,
756 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400757 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000758 dash_pad,
759 tag_footer1,
760 tag_footer2,
761 dash_pad,
762 ])
763 else:
764 return '\n'.join([
765 dash_pad,
766 tag_header,
767 tag_footer2,
768 dash_pad,
769 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000770
771
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700772def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700773 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000774 task_summary_json, task_output_dir, task_output_stdout,
775 include_perf):
maruela5490782015-09-30 10:56:59 -0700776 """Retrieves results of a Swarming task.
777
778 Returns:
779 process exit code that should be returned to the user.
780 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700781 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000782 output_collector = TaskOutputCollector(
783 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700784
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700785 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700786 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400787 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700788 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400789 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400790 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000791 output_collector, include_perf,
792 (len(task_output_stdout) > 0),
793 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700794 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700795
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400796 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700797 shard_exit_code = metadata.get('exit_code')
798 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700799 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700800 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700801 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400802 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700803 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700804
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700805 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000806 s = decorate_shard_output(
807 swarming, index, metadata,
808 "console" in task_output_stdout).encode(
809 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700810 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400811 if len(seen_shards) < len(task_ids):
812 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700813 else:
maruel77f720b2015-09-15 12:35:22 -0700814 print('%s: %s %s' % (
815 metadata.get('bot_id', 'N/A'),
816 metadata['task_id'],
817 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000818 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700819 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400820 if output:
821 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700822 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700823 summary = output_collector.finalize()
824 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700825 # TODO(maruel): Make this optional.
826 for i in summary['shards']:
827 if i:
828 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700829 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700830
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400831 if decorate and total_duration:
832 print('Total duration: %.1fs' % total_duration)
833
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400834 if len(seen_shards) != len(task_ids):
835 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700836 print >> sys.stderr, ('Results from some shards are missing: %s' %
837 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700838 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700839
maruela5490782015-09-30 10:56:59 -0700840 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000841
842
maruel77f720b2015-09-15 12:35:22 -0700843### API management.
844
845
846class APIError(Exception):
847 pass
848
849
850def endpoints_api_discovery_apis(host):
851 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
852 the APIs exposed by a host.
853
854 https://developers.google.com/discovery/v1/reference/apis/list
855 """
maruel380e3262016-08-31 16:10:06 -0700856 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
857 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700858 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
859 if data is None:
860 raise APIError('Failed to discover APIs on %s' % host)
861 out = {}
862 for api in data['items']:
863 if api['id'] == 'discovery:v1':
864 continue
865 # URL is of the following form:
866 # url = host + (
867 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
868 api_data = net.url_read_json(api['discoveryRestUrl'])
869 if api_data is None:
870 raise APIError('Failed to discover %s on %s' % (api['id'], host))
871 out[api['id']] = api_data
872 return out
873
874
maruelaf6b06c2017-06-08 06:26:53 -0700875def get_yielder(base_url, limit):
876 """Returns the first query and a function that yields following items."""
877 CHUNK_SIZE = 250
878
879 url = base_url
880 if limit:
881 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
882 data = net.url_read_json(url)
883 if data is None:
884 # TODO(maruel): Do basic diagnostic.
885 raise Failure('Failed to access %s' % url)
886 org_cursor = data.pop('cursor', None)
887 org_total = len(data.get('items') or [])
888 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
889 if not org_cursor or not org_total:
890 # This is not an iterable resource.
891 return data, lambda: []
892
893 def yielder():
894 cursor = org_cursor
895 total = org_total
896 # Some items support cursors. Try to get automatically if cursors are needed
897 # by looking at the 'cursor' items.
898 while cursor and (not limit or total < limit):
899 merge_char = '&' if '?' in base_url else '?'
900 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
901 if limit:
902 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
903 new = net.url_read_json(url)
904 if new is None:
905 raise Failure('Failed to access %s' % url)
906 cursor = new.get('cursor')
907 new_items = new.get('items')
908 nb_items = len(new_items or [])
909 total += nb_items
910 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
911 yield new_items
912
913 return data, yielder
914
915
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500916### Commands.
917
918
919def abort_task(_swarming, _manifest):
920 """Given a task manifest that was triggered, aborts its execution."""
921 # TODO(vadimsh): No supported by the server yet.
922
923
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400924def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800925 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500926 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500927 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500928 dest='dimensions', metavar='FOO bar',
929 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500930 parser.add_option_group(parser.filter_group)
931
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400932
maruelaf6b06c2017-06-08 06:26:53 -0700933def process_filter_options(parser, options):
934 for key, value in options.dimensions:
935 if ':' in key:
936 parser.error('--dimension key cannot contain ":"')
937 if key.strip() != key:
938 parser.error('--dimension key has whitespace')
939 if not key:
940 parser.error('--dimension key is empty')
941
942 if value.strip() != value:
943 parser.error('--dimension value has whitespace')
944 if not value:
945 parser.error('--dimension value is empty')
946 options.dimensions.sort()
947
948
Vadim Shtayurab450c602014-05-12 19:23:25 -0700949def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400950 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700951 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700952 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700953 help='Number of shards to trigger and collect.')
954 parser.add_option_group(parser.sharding_group)
955
956
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400957def add_trigger_options(parser):
958 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500959 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400960 add_filter_options(parser)
961
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400962 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800963 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700964 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500965 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800966 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500967 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700968 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800969 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800970 '--env-prefix', default=[], action='append', nargs=2,
971 metavar='VAR local/path',
972 help='Prepend task-relative `local/path` to the task\'s VAR environment '
973 'variable using os-appropriate pathsep character. Can be specified '
974 'multiple times for the same VAR to add multiple paths.')
975 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400976 '--idempotent', action='store_true', default=False,
977 help='When set, the server will actively try to find a previous task '
978 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800979 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700980 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700981 help='The optional path to a file containing the secret_bytes to use with'
982 'this task.')
maruel681d6802017-01-17 16:56:03 -0800983 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700984 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400985 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800986 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700987 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400988 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800989 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500990 '--raw-cmd', action='store_true', default=False,
991 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700992 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800993 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500994 '--relative-cwd',
995 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
996 'requires --raw-cmd')
997 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700998 '--cipd-package', action='append', default=[], metavar='PKG',
999 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -07001000 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001001 group.add_option(
1002 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -07001003 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001004 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1005 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001006 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001007 help='Email of a service account to run the task as, or literal "bot" '
1008 'string to indicate that the task should use the same account the '
1009 'bot itself is using to authenticate to Swarming. Don\'t use task '
1010 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001011 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001012 '-o', '--output', action='append', default=[], metavar='PATH',
1013 help='A list of files to return in addition to those written to '
1014 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1015 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001016 group.add_option(
1017 '--wait-for-capacity', action='store_true', default=False,
1018 help='Instructs to leave the task PENDING even if there\'s no known bot '
1019 'that could run this task, otherwise the task will be denied with '
1020 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001021 parser.add_option_group(group)
1022
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001023 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001024 group.add_option(
1025 '--priority', type='int', default=100,
1026 help='The lower value, the more important the task is')
1027 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001028 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001029 help='Display name of the task. Defaults to '
1030 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1031 'isolated file is provided, if a hash is provided, it defaults to '
1032 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1033 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001034 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001035 help='Tags to assign to the task.')
1036 group.add_option(
1037 '--user', default='',
1038 help='User associated with the task. Defaults to authenticated user on '
1039 'the server.')
1040 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001041 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001042 help='Seconds to allow the task to be pending for a bot to run before '
1043 'this task request expires.')
1044 group.add_option(
1045 '--deadline', type='int', dest='expiration',
1046 help=optparse.SUPPRESS_HELP)
1047 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001048
1049
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001050def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001051 """Processes trigger options and does preparatory steps.
1052
1053 Returns:
1054 NewTaskRequest instance.
1055 """
maruelaf6b06c2017-06-08 06:26:53 -07001056 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001057 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001058 if args and args[0] == '--':
1059 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001060
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001061 if not options.dimensions:
1062 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001063 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1064 parser.error('--tags must be in the format key:value')
1065 if options.raw_cmd and not args:
1066 parser.error(
1067 'Arguments with --raw-cmd should be passed after -- as command '
1068 'delimiter.')
1069 if options.isolate_server and not options.namespace:
1070 parser.error(
1071 '--namespace must be a valid value when --isolate-server is used')
1072 if not options.isolated and not options.raw_cmd:
1073 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1074
1075 # Isolated
1076 # --isolated is required only if --raw-cmd wasn't provided.
1077 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1078 # preferred server.
1079 isolateserver.process_isolate_server_options(
1080 parser, options, False, not options.raw_cmd)
1081 inputs_ref = None
1082 if options.isolate_server:
1083 inputs_ref = FilesRef(
1084 isolated=options.isolated,
1085 isolatedserver=options.isolate_server,
1086 namespace=options.namespace)
1087
1088 # Command
1089 command = None
1090 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001091 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001092 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001093 if options.relative_cwd:
1094 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1095 if not a.startswith(os.getcwd()):
1096 parser.error(
1097 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001098 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001099 if options.relative_cwd:
1100 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001101 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001102
maruel0a25f6c2017-05-10 10:43:23 -07001103 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001104 cipd_packages = []
1105 for p in options.cipd_package:
1106 split = p.split(':', 2)
1107 if len(split) != 3:
1108 parser.error('CIPD packages must take the form: path:package:version')
1109 cipd_packages.append(CipdPackage(
1110 package_name=split[1],
1111 path=split[0],
1112 version=split[2]))
1113 cipd_input = None
1114 if cipd_packages:
1115 cipd_input = CipdInput(
1116 client_package=None,
1117 packages=cipd_packages,
1118 server=None)
1119
maruel0a25f6c2017-05-10 10:43:23 -07001120 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001121 secret_bytes = None
1122 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001123 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001124 secret_bytes = f.read().encode('base64')
1125
maruel0a25f6c2017-05-10 10:43:23 -07001126 # Named caches
maruel681d6802017-01-17 16:56:03 -08001127 caches = [
1128 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1129 for i in options.named_cache
1130 ]
maruel0a25f6c2017-05-10 10:43:23 -07001131
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001132 env_prefixes = {}
1133 for k, v in options.env_prefix:
1134 env_prefixes.setdefault(k, []).append(v)
1135
maruel77f720b2015-09-15 12:35:22 -07001136 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001137 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001138 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001139 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001140 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001141 dimensions=options.dimensions,
1142 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001143 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001144 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001145 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001146 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001147 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001148 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001149 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001150 outputs=options.output,
1151 secret_bytes=secret_bytes)
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001152 task_slice = TaskSlice(
1153 expiration_secs=options.expiration,
1154 properties=properties,
1155 wait_for_capacity=options.wait_for_capacity)
maruel77f720b2015-09-15 12:35:22 -07001156 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001157 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001158 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001159 priority=options.priority,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001160 task_slices=[task_slice],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001161 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001162 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001163 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001164
1165
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001166class TaskOutputStdoutOption(optparse.Option):
1167 """Where to output the each task's console output (stderr/stdout).
1168
1169 The output will be;
1170 none - not be downloaded.
1171 json - stored in summary.json file *only*.
1172 console - shown on stdout *only*.
1173 all - stored in summary.json and shown on stdout.
1174 """
1175
1176 choices = ['all', 'json', 'console', 'none']
1177
1178 def __init__(self, *args, **kw):
1179 optparse.Option.__init__(
1180 self,
1181 *args,
1182 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001183 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001184 help=re.sub('\s\s*', ' ', self.__doc__),
1185 **kw)
1186
1187 def convert_value(self, opt, value):
1188 if value not in self.choices:
1189 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1190 self.get_opt_string(), self.choices, value))
1191 stdout_to = []
1192 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001193 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001194 elif value != 'none':
1195 stdout_to = [value]
1196 return stdout_to
1197
1198
maruel@chromium.org0437a732013-08-27 16:05:52 +00001199def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001200 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001201 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001202 help='Timeout to wait for result, set to -1 for no timeout and get '
1203 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001204 parser.group_logging.add_option(
1205 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001206 parser.group_logging.add_option(
1207 '--print-status-updates', action='store_true',
1208 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001209 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001210 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001211 '--task-summary-json',
1212 metavar='FILE',
1213 help='Dump a summary of task results to this file as json. It contains '
1214 'only shards statuses as know to server directly. Any output files '
1215 'emitted by the task can be collected by using --task-output-dir')
1216 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001217 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001218 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001219 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001220 'directory contains per-shard directory with output files produced '
1221 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001222 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001223 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001224 parser.task_output_group.add_option(
1225 '--perf', action='store_true', default=False,
1226 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001227 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001228
1229
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001230def process_collect_options(parser, options):
1231 # Only negative -1 is allowed, disallow other negative values.
1232 if options.timeout != -1 and options.timeout < 0:
1233 parser.error('Invalid --timeout value')
1234
1235
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001236@subcommand.usage('bots...')
1237def CMDbot_delete(parser, args):
1238 """Forcibly deletes bots from the Swarming server."""
1239 parser.add_option(
1240 '-f', '--force', action='store_true',
1241 help='Do not prompt for confirmation')
1242 options, args = parser.parse_args(args)
1243 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001244 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001245
1246 bots = sorted(args)
1247 if not options.force:
1248 print('Delete the following bots?')
1249 for bot in bots:
1250 print(' %s' % bot)
1251 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1252 print('Goodbye.')
1253 return 1
1254
1255 result = 0
1256 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001257 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001258 if net.url_read_json(url, data={}, method='POST') is None:
1259 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001260 result = 1
1261 return result
1262
1263
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001264def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001265 """Returns information about the bots connected to the Swarming server."""
1266 add_filter_options(parser)
1267 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001268 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001269 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001270 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001271 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001272 help='Keep both dead and alive bots')
1273 parser.filter_group.add_option(
1274 '--busy', action='store_true', help='Keep only busy bots')
1275 parser.filter_group.add_option(
1276 '--idle', action='store_true', help='Keep only idle bots')
1277 parser.filter_group.add_option(
1278 '--mp', action='store_true',
1279 help='Keep only Machine Provider managed bots')
1280 parser.filter_group.add_option(
1281 '--non-mp', action='store_true',
1282 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001283 parser.filter_group.add_option(
1284 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001285 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001286 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001287 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001288
1289 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001290 parser.error('Use only one of --keep-dead or --dead-only')
1291 if options.busy and options.idle:
1292 parser.error('Use only one of --busy or --idle')
1293 if options.mp and options.non_mp:
1294 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001295
smut281c3902018-05-30 17:50:05 -07001296 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001297 values = []
1298 if options.dead_only:
1299 values.append(('is_dead', 'TRUE'))
1300 elif options.keep_dead:
1301 values.append(('is_dead', 'NONE'))
1302 else:
1303 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001304
maruelaf6b06c2017-06-08 06:26:53 -07001305 if options.busy:
1306 values.append(('is_busy', 'TRUE'))
1307 elif options.idle:
1308 values.append(('is_busy', 'FALSE'))
1309 else:
1310 values.append(('is_busy', 'NONE'))
1311
1312 if options.mp:
1313 values.append(('is_mp', 'TRUE'))
1314 elif options.non_mp:
1315 values.append(('is_mp', 'FALSE'))
1316 else:
1317 values.append(('is_mp', 'NONE'))
1318
1319 for key, value in options.dimensions:
1320 values.append(('dimensions', '%s:%s' % (key, value)))
1321 url += urllib.urlencode(values)
1322 try:
1323 data, yielder = get_yielder(url, 0)
1324 bots = data.get('items') or []
1325 for items in yielder():
1326 if items:
1327 bots.extend(items)
1328 except Failure as e:
1329 sys.stderr.write('\n%s\n' % e)
1330 return 1
maruel77f720b2015-09-15 12:35:22 -07001331 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001332 print bot['bot_id']
1333 if not options.bare:
1334 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1335 print ' %s' % json.dumps(dimensions, sort_keys=True)
1336 if bot.get('task_id'):
1337 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001338 return 0
1339
1340
maruelfd0a90c2016-06-10 11:51:10 -07001341@subcommand.usage('task_id')
1342def CMDcancel(parser, args):
1343 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001344 parser.add_option(
1345 '-k', '--kill-running', action='store_true', default=False,
1346 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001347 options, args = parser.parse_args(args)
1348 if not args:
1349 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001350 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001351 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001352 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001353 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001354 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001355 print('Deleting %s failed. Probably already gone' % task_id)
1356 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001357 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001358 return 0
1359
1360
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001361@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001362def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001363 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001364
1365 The result can be in multiple part if the execution was sharded. It can
1366 potentially have retries.
1367 """
1368 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001369 parser.add_option(
1370 '-j', '--json',
1371 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001372 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001373 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001374 if not args and not options.json:
1375 parser.error('Must specify at least one task id or --json.')
1376 if args and options.json:
1377 parser.error('Only use one of task id or --json.')
1378
1379 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001380 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001381 try:
maruel1ceb3872015-10-14 06:10:44 -07001382 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001383 data = json.load(f)
1384 except (IOError, ValueError):
1385 parser.error('Failed to open %s' % options.json)
1386 try:
1387 tasks = sorted(
1388 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1389 args = [t['task_id'] for t in tasks]
1390 except (KeyError, TypeError):
1391 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001392 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001393 # Take in account all the task slices.
1394 offset = 0
1395 for s in data['request']['task_slices']:
1396 m = (offset + s['properties']['execution_timeout_secs'] +
1397 s['expiration_secs'])
1398 if m > options.timeout:
1399 options.timeout = m
1400 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001401 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001402 else:
1403 valid = frozenset('0123456789abcdef')
1404 if any(not valid.issuperset(task_id) for task_id in args):
1405 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001406
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001407 try:
1408 return collect(
1409 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001410 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001411 options.timeout,
1412 options.decorate,
1413 options.print_status_updates,
1414 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001415 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001416 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001417 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001418 except Failure:
1419 on_error.report(None)
1420 return 1
1421
1422
maruel77f720b2015-09-15 12:35:22 -07001423@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001424def CMDpost(parser, args):
1425 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1426
1427 Input data must be sent to stdin, result is printed to stdout.
1428
1429 If HTTP response code >= 400, returns non-zero.
1430 """
1431 options, args = parser.parse_args(args)
1432 if len(args) != 1:
1433 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001434 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001435 data = sys.stdin.read()
1436 try:
1437 resp = net.url_read(url, data=data, method='POST')
1438 except net.TimeoutError:
1439 sys.stderr.write('Timeout!\n')
1440 return 1
1441 if not resp:
1442 sys.stderr.write('No response!\n')
1443 return 1
1444 sys.stdout.write(resp)
1445 return 0
1446
1447
1448@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001449def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001450 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1451 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001452
1453 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001454 Raw task request and results:
1455 swarming.py query -S server-url.com task/123456/request
1456 swarming.py query -S server-url.com task/123456/result
1457
maruel77f720b2015-09-15 12:35:22 -07001458 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001459 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001460
maruelaf6b06c2017-06-08 06:26:53 -07001461 Listing last 10 tasks on a specific bot named 'bot1':
1462 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001463
maruelaf6b06c2017-06-08 06:26:53 -07001464 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001465 quoting is important!:
1466 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001467 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001468 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001469 parser.add_option(
1470 '-L', '--limit', type='int', default=200,
1471 help='Limit to enforce on limitless items (like number of tasks); '
1472 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001473 parser.add_option(
1474 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001475 parser.add_option(
1476 '--progress', action='store_true',
1477 help='Prints a dot at each request to show progress')
1478 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001479 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001480 parser.error(
1481 'Must specify only method name and optionally query args properly '
1482 'escaped.')
smut281c3902018-05-30 17:50:05 -07001483 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001484 try:
1485 data, yielder = get_yielder(base_url, options.limit)
1486 for items in yielder():
1487 if items:
1488 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001489 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001490 sys.stderr.write('.')
1491 sys.stderr.flush()
1492 except Failure as e:
1493 sys.stderr.write('\n%s\n' % e)
1494 return 1
maruel77f720b2015-09-15 12:35:22 -07001495 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001496 sys.stderr.write('\n')
1497 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001498 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001499 options.json = unicode(os.path.abspath(options.json))
1500 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001501 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001502 try:
maruel77f720b2015-09-15 12:35:22 -07001503 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001504 sys.stdout.write('\n')
1505 except IOError:
1506 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001507 return 0
1508
1509
maruel77f720b2015-09-15 12:35:22 -07001510def CMDquery_list(parser, args):
1511 """Returns list of all the Swarming APIs that can be used with command
1512 'query'.
1513 """
1514 parser.add_option(
1515 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1516 options, args = parser.parse_args(args)
1517 if args:
1518 parser.error('No argument allowed.')
1519
1520 try:
1521 apis = endpoints_api_discovery_apis(options.swarming)
1522 except APIError as e:
1523 parser.error(str(e))
1524 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001525 options.json = unicode(os.path.abspath(options.json))
1526 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001527 json.dump(apis, f)
1528 else:
1529 help_url = (
1530 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1531 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001532 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1533 if i:
1534 print('')
maruel77f720b2015-09-15 12:35:22 -07001535 print api_id
maruel11e31af2017-02-15 07:30:50 -08001536 print ' ' + api['description'].strip()
1537 if 'resources' in api:
1538 # Old.
1539 for j, (resource_name, resource) in enumerate(
1540 sorted(api['resources'].iteritems())):
1541 if j:
1542 print('')
1543 for method_name, method in sorted(resource['methods'].iteritems()):
1544 # Only list the GET ones.
1545 if method['httpMethod'] != 'GET':
1546 continue
1547 print '- %s.%s: %s' % (
1548 resource_name, method_name, method['path'])
1549 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001550 ' ' + l for l in textwrap.wrap(
1551 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001552 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1553 else:
1554 # New.
1555 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001556 # Only list the GET ones.
1557 if method['httpMethod'] != 'GET':
1558 continue
maruel11e31af2017-02-15 07:30:50 -08001559 print '- %s: %s' % (method['id'], method['path'])
1560 print('\n'.join(
1561 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001562 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1563 return 0
1564
1565
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001566@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001567def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001568 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001569
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001570 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001571 """
1572 add_trigger_options(parser)
1573 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001574 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001575 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001576 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001577 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001578 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001579 tasks = trigger_task_shards(
1580 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001581 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001582 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001583 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001584 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001585 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001586 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001587 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001588 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001589 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001590 task_ids = [
1591 t['task_id']
1592 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1593 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001594 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001595 offset = 0
1596 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001597 m = (offset + s.properties.execution_timeout_secs +
1598 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001599 if m > options.timeout:
1600 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001601 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001602 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001603 try:
1604 return collect(
1605 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001606 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001607 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001608 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001609 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001610 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001611 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001612 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001613 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001614 except Failure:
1615 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001616 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001617
1618
maruel18122c62015-10-23 06:31:23 -07001619@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001620def CMDreproduce(parser, args):
1621 """Runs a task locally that was triggered on the server.
1622
1623 This running locally the same commands that have been run on the bot. The data
1624 downloaded will be in a subdirectory named 'work' of the current working
1625 directory.
maruel18122c62015-10-23 06:31:23 -07001626
1627 You can pass further additional arguments to the target command by passing
1628 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001629 """
maruelc070e672016-02-22 17:32:57 -08001630 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001631 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001632 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001633 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001634 extra_args = []
1635 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001636 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001637 if len(args) > 1:
1638 if args[1] == '--':
1639 if len(args) > 2:
1640 extra_args = args[2:]
1641 else:
1642 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001643
smut281c3902018-05-30 17:50:05 -07001644 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001645 request = net.url_read_json(url)
1646 if not request:
1647 print >> sys.stderr, 'Failed to retrieve request data for the task'
1648 return 1
1649
maruel12e30012015-10-09 11:55:35 -07001650 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001651 if fs.isdir(workdir):
1652 parser.error('Please delete the directory \'work\' first')
1653 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001654 cachedir = unicode(os.path.abspath('cipd_cache'))
1655 if not fs.exists(cachedir):
1656 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001657
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001658 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001659 env = os.environ.copy()
1660 env['SWARMING_BOT_ID'] = 'reproduce'
1661 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001662 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001663 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001664 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001665 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001666 if not i['value']:
1667 env.pop(key, None)
1668 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001669 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001670
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001671 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001672 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001673 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001674 for i in env_prefixes:
1675 key = i['key']
1676 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001677 cur = env.get(key)
1678 if cur:
1679 paths.append(cur)
1680 env[key] = os.path.pathsep.join(paths)
1681
iannucci31ab9192017-05-02 19:11:56 -07001682 command = []
nodir152cba62016-05-12 16:08:56 -07001683 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001684 # Create the tree.
1685 with isolateserver.get_storage(
1686 properties['inputs_ref']['isolatedserver'],
1687 properties['inputs_ref']['namespace']) as storage:
1688 bundle = isolateserver.fetch_isolated(
1689 properties['inputs_ref']['isolated'],
1690 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -04001691 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001692 workdir,
1693 False)
maruel29ab2fd2015-10-16 11:44:01 -07001694 command = bundle.command
1695 if bundle.relative_cwd:
1696 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001697 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001698
1699 if properties.get('command'):
1700 command.extend(properties['command'])
1701
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001702 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001703 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001704 if not options.output_dir:
1705 new_command = run_isolated.process_command(command, 'invalid', None)
1706 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001707 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001708 else:
1709 # Make the path absolute, as the process will run from a subdirectory.
1710 options.output_dir = os.path.abspath(options.output_dir)
1711 new_command = run_isolated.process_command(
1712 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001713 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001714 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001715 command = new_command
1716 file_path.ensure_command_has_abs_path(command, workdir)
1717
1718 if properties.get('cipd_input'):
1719 ci = properties['cipd_input']
1720 cp = ci['client_package']
1721 client_manager = cipd.get_client(
1722 ci['server'], cp['package_name'], cp['version'], cachedir)
1723
1724 with client_manager as client:
1725 by_path = collections.defaultdict(list)
1726 for pkg in ci['packages']:
1727 path = pkg['path']
1728 # cipd deals with 'root' as ''
1729 if path == '.':
1730 path = ''
1731 by_path[path].append((pkg['package_name'], pkg['version']))
1732 client.ensure(workdir, by_path, cache_dir=cachedir)
1733
maruel77f720b2015-09-15 12:35:22 -07001734 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001735 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001736 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001737 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001738 print >> sys.stderr, str(e)
1739 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001740
1741
maruel0eb1d1b2015-10-02 14:48:21 -07001742@subcommand.usage('bot_id')
1743def CMDterminate(parser, args):
1744 """Tells a bot to gracefully shut itself down as soon as it can.
1745
1746 This is done by completing whatever current task there is then exiting the bot
1747 process.
1748 """
1749 parser.add_option(
1750 '--wait', action='store_true', help='Wait for the bot to terminate')
1751 options, args = parser.parse_args(args)
1752 if len(args) != 1:
1753 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001754 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001755 request = net.url_read_json(url, data={})
1756 if not request:
1757 print >> sys.stderr, 'Failed to ask for termination'
1758 return 1
1759 if options.wait:
1760 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001761 options.swarming,
1762 [request['task_id']],
1763 0.,
1764 False,
1765 False,
1766 None,
1767 None,
1768 [],
maruel9531ce02016-04-13 06:11:23 -07001769 False)
maruelbfc5f872017-06-10 16:43:17 -07001770 else:
1771 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001772 return 0
1773
1774
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001775@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001776def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001777 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001778
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001779 Passes all extra arguments provided after '--' as additional command line
1780 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001781 """
1782 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001783 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001784 parser.add_option(
1785 '--dump-json',
1786 metavar='FILE',
1787 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001788 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001789 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001790 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001791 tasks = trigger_task_shards(
1792 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001793 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001794 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001795 tasks_sorted = sorted(
1796 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001797 if options.dump_json:
1798 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001799 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001800 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001801 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001802 }
maruel46b015f2015-10-13 18:40:35 -07001803 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001804 print('To collect results, use:')
1805 print(' swarming.py collect -S %s --json %s' %
1806 (options.swarming, options.dump_json))
1807 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001808 print('To collect results, use:')
1809 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001810 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1811 print('Or visit:')
1812 for t in tasks_sorted:
1813 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001814 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001815 except Failure:
1816 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001817 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001818
1819
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001820class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001821 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001822 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001823 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001824 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001825 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001826 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001827 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001828 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001829 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001830 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001831
1832 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001833 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001834 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001835 auth.process_auth_options(self, options)
1836 user = self._process_swarming(options)
1837 if hasattr(options, 'user') and not options.user:
1838 options.user = user
1839 return options, args
1840
1841 def _process_swarming(self, options):
1842 """Processes the --swarming option and aborts if not specified.
1843
1844 Returns the identity as determined by the server.
1845 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001846 if not options.swarming:
1847 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001848 try:
1849 options.swarming = net.fix_url(options.swarming)
1850 except ValueError as e:
1851 self.error('--swarming %s' % e)
1852 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001853 try:
1854 user = auth.ensure_logged_in(options.swarming)
1855 except ValueError as e:
1856 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001857 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001858
1859
1860def main(args):
1861 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001862 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001863
1864
1865if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001866 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001867 fix_encoding.fix_encoding()
1868 tools.disable_buffering()
1869 colorama.init()
1870 sys.exit(main(sys.argv[1:]))