blob: 4a6f4c10803637447e734c8fc63abfc166f7d705 [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005
6"""Client tool to trigger tasks or retrieve results from a Swarming server."""
7
Robert Iannuccifafa7352018-06-13 17:08:17 +00008__version__ = '0.13'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import sys
maruel11e31af2017-02-15 07:30:50 -080018import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
iannucci31ab9192017-05-02 19:11:56 -070038import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +000040import isolated_format
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040041import local_caching
maruelc070e672016-02-22 17:32:57 -080042import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000043
44
tansella4949442016-06-23 22:34:32 -070045ROOT_DIR = os.path.dirname(os.path.abspath(
46 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050047
48
49class Failure(Exception):
50 """Generic failure."""
51 pass
52
53
maruel0a25f6c2017-05-10 10:43:23 -070054def default_task_name(options):
55 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050056 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070057 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070058 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070059 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070060 if options.isolated:
61 task_name += u'/' + options.isolated
62 return task_name
63 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050064
65
66### Triggering.
67
68
maruel77f720b2015-09-15 12:35:22 -070069# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070070CipdPackage = collections.namedtuple(
71 'CipdPackage',
72 [
73 'package_name',
74 'path',
75 'version',
76 ])
77
78
79# See ../appengine/swarming/swarming_rpcs.py.
80CipdInput = collections.namedtuple(
81 'CipdInput',
82 [
83 'client_package',
84 'packages',
85 'server',
86 ])
87
88
89# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070090FilesRef = collections.namedtuple(
91 'FilesRef',
92 [
93 'isolated',
94 'isolatedserver',
95 'namespace',
96 ])
97
98
99# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800100StringListPair = collections.namedtuple(
101 'StringListPair', [
102 'key',
103 'value', # repeated string
104 ]
105)
106
107
108# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700109TaskProperties = collections.namedtuple(
110 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500111 [
maruel681d6802017-01-17 16:56:03 -0800112 'caches',
borenet02f772b2016-06-22 12:42:19 -0700113 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500114 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500115 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500116 'dimensions',
117 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800118 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700119 'execution_timeout_secs',
120 'extra_args',
121 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500122 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700123 'inputs_ref',
124 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700125 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700126 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700127 ])
128
129
130# See ../appengine/swarming/swarming_rpcs.py.
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400131TaskSlice = collections.namedtuple(
132 'TaskSlice',
133 [
134 'expiration_secs',
135 'properties',
136 'wait_for_capacity',
137 ])
138
139
140# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700141NewTaskRequest = collections.namedtuple(
142 'NewTaskRequest',
143 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144 'name',
maruel77f720b2015-09-15 12:35:22 -0700145 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500146 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400147 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700148 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500149 'tags',
150 'user',
Robert Iannuccifafa7352018-06-13 17:08:17 +0000151 'pool_task_template',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500152 ])
153
154
maruel77f720b2015-09-15 12:35:22 -0700155def namedtuple_to_dict(value):
156 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400157 if hasattr(value, '_asdict'):
158 return namedtuple_to_dict(value._asdict())
159 if isinstance(value, (list, tuple)):
160 return [namedtuple_to_dict(v) for v in value]
161 if isinstance(value, dict):
162 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
163 return value
maruel77f720b2015-09-15 12:35:22 -0700164
165
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700166def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800167 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700168
169 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170 """
maruel77f720b2015-09-15 12:35:22 -0700171 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700172 # Don't send 'service_account' if it is None to avoid confusing older
173 # version of the server that doesn't know about 'service_account' and don't
174 # use it at all.
175 if not out['service_account']:
176 out.pop('service_account')
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400177 out['task_slices'][0]['properties']['dimensions'] = [
maruel77f720b2015-09-15 12:35:22 -0700178 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400179 for k, v in out['task_slices'][0]['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700180 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400181 out['task_slices'][0]['properties']['env'] = [
maruel77f720b2015-09-15 12:35:22 -0700182 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400183 for k, v in out['task_slices'][0]['properties']['env'].iteritems()
maruel77f720b2015-09-15 12:35:22 -0700184 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400185 out['task_slices'][0]['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700186 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500187
188
maruel77f720b2015-09-15 12:35:22 -0700189def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500190 """Triggers a request on the Swarming server and returns the json data.
191
192 It's the low-level function.
193
194 Returns:
195 {
196 'request': {
197 'created_ts': u'2010-01-02 03:04:05',
198 'name': ..
199 },
200 'task_id': '12300',
201 }
202 """
203 logging.info('Triggering: %s', raw_request['name'])
204
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500205 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700206 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500207 if not result:
208 on_error.report('Failed to trigger task %s' % raw_request['name'])
209 return None
maruele557bce2015-11-17 09:01:27 -0800210 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800211 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800212 msg = 'Failed to trigger task %s' % raw_request['name']
213 if result['error'].get('errors'):
214 for err in result['error']['errors']:
215 if err.get('message'):
216 msg += '\nMessage: %s' % err['message']
217 if err.get('debugInfo'):
218 msg += '\nDebug info:\n%s' % err['debugInfo']
219 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800220 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800221
222 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800223 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500224 return result
225
226
227def setup_googletest(env, shards, index):
228 """Sets googletest specific environment variables."""
229 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700230 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
231 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
232 env = env[:]
233 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
234 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500235 return env
236
237
238def trigger_task_shards(swarming, task_request, shards):
239 """Triggers one or many subtasks of a sharded task.
240
241 Returns:
242 Dict with task details, returned to caller as part of --dump-json output.
243 None in case of failure.
244 """
245 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700246 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500247 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400248 req['task_slices'][0]['properties']['env'] = setup_googletest(
249 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700250 req['name'] += ':%s:%s' % (index, shards)
251 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500252
253 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500254 tasks = {}
255 priority_warning = False
256 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700257 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500258 if not task:
259 break
260 logging.info('Request result: %s', task)
261 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400262 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500263 priority_warning = True
264 print >> sys.stderr, (
265 'Priority was reset to %s' % task['request']['priority'])
266 tasks[request['name']] = {
267 'shard_index': index,
268 'task_id': task['task_id'],
269 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
270 }
271
272 # Some shards weren't triggered. Abort everything.
273 if len(tasks) != len(requests):
274 if tasks:
275 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
276 len(tasks), len(requests))
277 for task_dict in tasks.itervalues():
278 abort_task(swarming, task_dict['task_id'])
279 return None
280
281 return tasks
282
283
284### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000285
286
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700287# How often to print status updates to stdout in 'collect'.
288STATUS_UPDATE_INTERVAL = 15 * 60.
289
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400290
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400291class State(object):
292 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000293
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400294 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
295 values are part of the API so if they change, the API changed.
296
297 It's in fact an enum. Values should be in decreasing order of importance.
298 """
299 RUNNING = 0x10
300 PENDING = 0x20
301 EXPIRED = 0x30
302 TIMED_OUT = 0x40
303 BOT_DIED = 0x50
304 CANCELED = 0x60
305 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400306 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400307 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400308
maruel77f720b2015-09-15 12:35:22 -0700309 STATES = (
310 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400311 'COMPLETED', 'KILLED', 'NO_RESOURCE')
maruel77f720b2015-09-15 12:35:22 -0700312 STATES_RUNNING = ('RUNNING', 'PENDING')
313 STATES_NOT_RUNNING = (
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400314 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED', 'KILLED',
315 'NO_RESOURCE')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400316 STATES_DONE = ('TIMED_OUT', 'COMPLETED', 'KILLED')
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400317 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED', 'NO_RESOURCE')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400318
319 _NAMES = {
320 RUNNING: 'Running',
321 PENDING: 'Pending',
322 EXPIRED: 'Expired',
323 TIMED_OUT: 'Execution timed out',
324 BOT_DIED: 'Bot died',
325 CANCELED: 'User canceled',
326 COMPLETED: 'Completed',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400327 KILLED: 'User killed',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400328 NO_RESOURCE: 'No resource',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400329 }
330
maruel77f720b2015-09-15 12:35:22 -0700331 _ENUMS = {
332 'RUNNING': RUNNING,
333 'PENDING': PENDING,
334 'EXPIRED': EXPIRED,
335 'TIMED_OUT': TIMED_OUT,
336 'BOT_DIED': BOT_DIED,
337 'CANCELED': CANCELED,
338 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400339 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400340 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700341 }
342
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400343 @classmethod
344 def to_string(cls, state):
345 """Returns a user-readable string representing a State."""
346 if state not in cls._NAMES:
347 raise ValueError('Invalid state %s' % state)
348 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000349
maruel77f720b2015-09-15 12:35:22 -0700350 @classmethod
351 def from_enum(cls, state):
352 """Returns int value based on the string."""
353 if state not in cls._ENUMS:
354 raise ValueError('Invalid state %s' % state)
355 return cls._ENUMS[state]
356
maruel@chromium.org0437a732013-08-27 16:05:52 +0000357
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700358class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700359 """Assembles task execution summary (for --task-summary-json output).
360
361 Optionally fetches task outputs from isolate server to local disk (used when
362 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363
364 This object is shared among multiple threads running 'retrieve_results'
365 function, in particular they call 'process_shard_result' method in parallel.
366 """
367
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000368 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
370
371 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700372 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700373 shard_count: expected number of task shards.
374 """
maruel12e30012015-10-09 11:55:35 -0700375 self.task_output_dir = (
376 unicode(os.path.abspath(task_output_dir))
377 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000378 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700379 self.shard_count = shard_count
380
381 self._lock = threading.Lock()
382 self._per_shard_results = {}
383 self._storage = None
384
nodire5028a92016-04-29 14:38:21 -0700385 if self.task_output_dir:
386 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387
Vadim Shtayurab450c602014-05-12 19:23:25 -0700388 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700389 """Stores results of a single task shard, fetches output files if necessary.
390
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400391 Modifies |result| in place.
392
maruel77f720b2015-09-15 12:35:22 -0700393 shard_index is 0-based.
394
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700395 Called concurrently from multiple threads.
396 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700397 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700398 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399 if shard_index < 0 or shard_index >= self.shard_count:
400 logging.warning(
401 'Shard index %d is outside of expected range: [0; %d]',
402 shard_index, self.shard_count - 1)
403 return
404
maruel77f720b2015-09-15 12:35:22 -0700405 if result.get('outputs_ref'):
406 ref = result['outputs_ref']
407 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
408 ref['isolatedserver'],
409 urllib.urlencode(
410 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400411
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700412 # Store result dict of that shard, ignore results we've already seen.
413 with self._lock:
414 if shard_index in self._per_shard_results:
415 logging.warning('Ignoring duplicate shard index %d', shard_index)
416 return
417 self._per_shard_results[shard_index] = result
418
419 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700420 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400421 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700422 result['outputs_ref']['isolatedserver'],
423 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400424 if storage:
425 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400426 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
427 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400428 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700429 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400430 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400431 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700432 os.path.join(self.task_output_dir, str(shard_index)),
433 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700434
435 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700436 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700437 with self._lock:
438 # Write an array of shard results with None for missing shards.
439 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700440 'shards': [
441 self._per_shard_results.get(i) for i in xrange(self.shard_count)
442 ],
443 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000444
445 # Don't store stdout in the summary if not requested too.
446 if "json" not in self.task_output_stdout:
447 for shard_json in summary['shards']:
448 if not shard_json:
449 continue
450 if "output" in shard_json:
451 del shard_json["output"]
452 if "outputs" in shard_json:
453 del shard_json["outputs"]
454
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700455 # Write summary.json to task_output_dir as well.
456 if self.task_output_dir:
457 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700458 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700459 summary,
460 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700461 if self._storage:
462 self._storage.close()
463 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700464 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700465
466 def _get_storage(self, isolate_server, namespace):
467 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700468 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700469 with self._lock:
470 if not self._storage:
471 self._storage = isolateserver.get_storage(isolate_server, namespace)
472 else:
473 # Shards must all use exact same isolate server and namespace.
474 if self._storage.location != isolate_server:
475 logging.error(
476 'Task shards are using multiple isolate servers: %s and %s',
477 self._storage.location, isolate_server)
478 return None
479 if self._storage.namespace != namespace:
480 logging.error(
481 'Task shards are using multiple namespaces: %s and %s',
482 self._storage.namespace, namespace)
483 return None
484 return self._storage
485
486
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500487def now():
488 """Exists so it can be mocked easily."""
489 return time.time()
490
491
maruel77f720b2015-09-15 12:35:22 -0700492def parse_time(value):
493 """Converts serialized time from the API to datetime.datetime."""
494 # When microseconds are 0, the '.123456' suffix is elided. This means the
495 # serialized format is not consistent, which confuses the hell out of python.
496 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
497 try:
498 return datetime.datetime.strptime(value, fmt)
499 except ValueError:
500 pass
501 raise ValueError('Failed to parse %s' % value)
502
503
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700504def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700505 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000506 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400507 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700508
Vadim Shtayurab450c602014-05-12 19:23:25 -0700509 Returns:
510 <result dict> on success.
511 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700512 """
maruel71c61c82016-02-22 06:52:05 -0800513 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700514 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700515 if include_perf:
516 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700517 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700518 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400519 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700520 attempt = 0
521
522 while not should_stop.is_set():
523 attempt += 1
524
525 # Waiting for too long -> give up.
526 current_time = now()
527 if deadline and current_time >= deadline:
528 logging.error('retrieve_results(%s) timed out on attempt %d',
529 base_url, attempt)
530 return None
531
532 # Do not spin too fast. Spin faster at the beginning though.
533 # Start with 1 sec delay and for each 30 sec of waiting add another second
534 # of delay, until hitting 15 sec ceiling.
535 if attempt > 1:
536 max_delay = min(15, 1 + (current_time - started) / 30.0)
537 delay = min(max_delay, deadline - current_time) if deadline else max_delay
538 if delay > 0:
539 logging.debug('Waiting %.1f sec before retrying', delay)
540 should_stop.wait(delay)
541 if should_stop.is_set():
542 return None
543
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400544 # Disable internal retries in net.url_read_json, since we are doing retries
545 # ourselves.
546 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700547 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
548 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400549 # Retry on 500s only if no timeout is specified.
550 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400551 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400552 if timeout == -1:
553 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400554 continue
maruel77f720b2015-09-15 12:35:22 -0700555
maruelbf53e042015-12-01 15:00:51 -0800556 if result.get('error'):
557 # An error occurred.
558 if result['error'].get('errors'):
559 for err in result['error']['errors']:
560 logging.warning(
561 'Error while reading task: %s; %s',
562 err.get('message'), err.get('debugInfo'))
563 elif result['error'].get('message'):
564 logging.warning(
565 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400566 if timeout == -1:
567 return result
maruelbf53e042015-12-01 15:00:51 -0800568 continue
569
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400570 # When timeout == -1, always return on first attempt. 500s are already
571 # retried in this case.
572 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000573 if fetch_stdout:
574 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700575 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700576 # Record the result, try to fetch attached output files (if any).
577 if output_collector:
578 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700579 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700580 if result.get('internal_failure'):
581 logging.error('Internal error!')
582 elif result['state'] == 'BOT_DIED':
583 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700584 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000585
586
maruel77f720b2015-09-15 12:35:22 -0700587def convert_to_old_format(result):
588 """Converts the task result data from Endpoints API format to old API format
589 for compatibility.
590
591 This goes into the file generated as --task-summary-json.
592 """
593 # Sets default.
594 result.setdefault('abandoned_ts', None)
595 result.setdefault('bot_id', None)
596 result.setdefault('bot_version', None)
597 result.setdefault('children_task_ids', [])
598 result.setdefault('completed_ts', None)
599 result.setdefault('cost_saved_usd', None)
600 result.setdefault('costs_usd', None)
601 result.setdefault('deduped_from', None)
602 result.setdefault('name', None)
603 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700604 result.setdefault('server_versions', None)
605 result.setdefault('started_ts', None)
606 result.setdefault('tags', None)
607 result.setdefault('user', None)
608
609 # Convertion back to old API.
610 duration = result.pop('duration', None)
611 result['durations'] = [duration] if duration else []
612 exit_code = result.pop('exit_code', None)
613 result['exit_codes'] = [int(exit_code)] if exit_code else []
614 result['id'] = result.pop('task_id')
615 result['isolated_out'] = result.get('outputs_ref', None)
616 output = result.pop('output', None)
617 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700618 # server_version
619 # Endpoints result 'state' as string. For compatibility with old code, convert
620 # to int.
621 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700622 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700623 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700624 if 'bot_dimensions' in result:
625 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700626 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700627 }
628 else:
629 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700630
631
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700632def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400633 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000634 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500635 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000636
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700637 Duplicate shards are ignored. Shards are yielded in order of completion.
638 Timed out shards are NOT yielded at all. Caller can compare number of yielded
639 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640
641 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500642 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 +0000643 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500644
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700645 output_collector is an optional instance of TaskOutputCollector that will be
646 used to fetch files produced by a task from isolate server to the local disk.
647
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500648 Yields:
649 (index, result). In particular, 'result' is defined as the
650 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000651 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000652 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400653 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700654 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700655 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700656
maruel@chromium.org0437a732013-08-27 16:05:52 +0000657 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
658 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700659 # Adds a task to the thread pool to call 'retrieve_results' and return
660 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400661 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700662 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000663 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400664 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000665 task_id, timeout, should_stop, output_collector, include_perf,
666 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700667
668 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400669 for shard_index, task_id in enumerate(task_ids):
670 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700671
672 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400673 shards_remaining = range(len(task_ids))
674 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700675 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700676 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700677 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700678 shard_index, result = results_channel.pull(
679 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700680 except threading_utils.TaskChannel.Timeout:
681 if print_status_updates:
682 print(
683 'Waiting for results from the following shards: %s' %
684 ', '.join(map(str, shards_remaining)))
685 sys.stdout.flush()
686 continue
687 except Exception:
688 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700689
690 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700691 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000692 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500693 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000694 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700695
Vadim Shtayurab450c602014-05-12 19:23:25 -0700696 # Yield back results to the caller.
697 assert shard_index in shards_remaining
698 shards_remaining.remove(shard_index)
699 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700700
maruel@chromium.org0437a732013-08-27 16:05:52 +0000701 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700702 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000703 should_stop.set()
704
705
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000706def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000707 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700708 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400709 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700710 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
711 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400712 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
713 metadata.get('abandoned_ts')):
714 pending = '%.1fs' % (
715 parse_time(metadata['abandoned_ts']) -
716 parse_time(metadata['created_ts'])
717 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400718 else:
719 pending = 'N/A'
720
maruel77f720b2015-09-15 12:35:22 -0700721 if metadata.get('duration') is not None:
722 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400723 else:
724 duration = 'N/A'
725
maruel77f720b2015-09-15 12:35:22 -0700726 if metadata.get('exit_code') is not None:
727 # Integers are encoded as string to not loose precision.
728 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400729 else:
730 exit_code = 'N/A'
731
732 bot_id = metadata.get('bot_id') or 'N/A'
733
maruel77f720b2015-09-15 12:35:22 -0700734 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400735 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000736 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400737 if metadata.get('state') == 'CANCELED':
738 tag_footer2 = ' Pending: %s CANCELED' % pending
739 elif metadata.get('state') == 'EXPIRED':
740 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400741 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400742 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
743 pending, duration, bot_id, exit_code, metadata['state'])
744 else:
745 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
746 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400747
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000748 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
749 dash_pad = '+-%s-+' % ('-' * tag_len)
750 tag_header = '| %s |' % tag_header.ljust(tag_len)
751 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
752 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400753
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000754 if include_stdout:
755 return '\n'.join([
756 dash_pad,
757 tag_header,
758 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400759 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000760 dash_pad,
761 tag_footer1,
762 tag_footer2,
763 dash_pad,
764 ])
765 else:
766 return '\n'.join([
767 dash_pad,
768 tag_header,
769 tag_footer2,
770 dash_pad,
771 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000772
773
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700774def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700775 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000776 task_summary_json, task_output_dir, task_output_stdout,
777 include_perf):
maruela5490782015-09-30 10:56:59 -0700778 """Retrieves results of a Swarming task.
779
780 Returns:
781 process exit code that should be returned to the user.
782 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700783 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000784 output_collector = TaskOutputCollector(
785 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700786
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700787 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700788 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400789 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400791 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400792 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000793 output_collector, include_perf,
794 (len(task_output_stdout) > 0),
795 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700796 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700797
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400798 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700799 shard_exit_code = metadata.get('exit_code')
800 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700801 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700802 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700803 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400804 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700805 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700806
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700807 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000808 s = decorate_shard_output(
809 swarming, index, metadata,
810 "console" in task_output_stdout).encode(
811 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700812 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400813 if len(seen_shards) < len(task_ids):
814 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700815 else:
maruel77f720b2015-09-15 12:35:22 -0700816 print('%s: %s %s' % (
817 metadata.get('bot_id', 'N/A'),
818 metadata['task_id'],
819 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000820 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700821 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400822 if output:
823 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700824 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700825 summary = output_collector.finalize()
826 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700827 # TODO(maruel): Make this optional.
828 for i in summary['shards']:
829 if i:
830 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700831 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700832
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400833 if decorate and total_duration:
834 print('Total duration: %.1fs' % total_duration)
835
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400836 if len(seen_shards) != len(task_ids):
837 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700838 print >> sys.stderr, ('Results from some shards are missing: %s' %
839 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700840 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700841
maruela5490782015-09-30 10:56:59 -0700842 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000843
844
maruel77f720b2015-09-15 12:35:22 -0700845### API management.
846
847
848class APIError(Exception):
849 pass
850
851
852def endpoints_api_discovery_apis(host):
853 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
854 the APIs exposed by a host.
855
856 https://developers.google.com/discovery/v1/reference/apis/list
857 """
maruel380e3262016-08-31 16:10:06 -0700858 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
859 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700860 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
861 if data is None:
862 raise APIError('Failed to discover APIs on %s' % host)
863 out = {}
864 for api in data['items']:
865 if api['id'] == 'discovery:v1':
866 continue
867 # URL is of the following form:
868 # url = host + (
869 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
870 api_data = net.url_read_json(api['discoveryRestUrl'])
871 if api_data is None:
872 raise APIError('Failed to discover %s on %s' % (api['id'], host))
873 out[api['id']] = api_data
874 return out
875
876
maruelaf6b06c2017-06-08 06:26:53 -0700877def get_yielder(base_url, limit):
878 """Returns the first query and a function that yields following items."""
879 CHUNK_SIZE = 250
880
881 url = base_url
882 if limit:
883 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
884 data = net.url_read_json(url)
885 if data is None:
886 # TODO(maruel): Do basic diagnostic.
887 raise Failure('Failed to access %s' % url)
888 org_cursor = data.pop('cursor', None)
889 org_total = len(data.get('items') or [])
890 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
891 if not org_cursor or not org_total:
892 # This is not an iterable resource.
893 return data, lambda: []
894
895 def yielder():
896 cursor = org_cursor
897 total = org_total
898 # Some items support cursors. Try to get automatically if cursors are needed
899 # by looking at the 'cursor' items.
900 while cursor and (not limit or total < limit):
901 merge_char = '&' if '?' in base_url else '?'
902 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
903 if limit:
904 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
905 new = net.url_read_json(url)
906 if new is None:
907 raise Failure('Failed to access %s' % url)
908 cursor = new.get('cursor')
909 new_items = new.get('items')
910 nb_items = len(new_items or [])
911 total += nb_items
912 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
913 yield new_items
914
915 return data, yielder
916
917
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500918### Commands.
919
920
921def abort_task(_swarming, _manifest):
922 """Given a task manifest that was triggered, aborts its execution."""
923 # TODO(vadimsh): No supported by the server yet.
924
925
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400926def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800927 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500928 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500929 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500930 dest='dimensions', metavar='FOO bar',
931 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500932 parser.add_option_group(parser.filter_group)
933
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400934
maruelaf6b06c2017-06-08 06:26:53 -0700935def process_filter_options(parser, options):
936 for key, value in options.dimensions:
937 if ':' in key:
938 parser.error('--dimension key cannot contain ":"')
939 if key.strip() != key:
940 parser.error('--dimension key has whitespace')
941 if not key:
942 parser.error('--dimension key is empty')
943
944 if value.strip() != value:
945 parser.error('--dimension value has whitespace')
946 if not value:
947 parser.error('--dimension value is empty')
948 options.dimensions.sort()
949
950
Vadim Shtayurab450c602014-05-12 19:23:25 -0700951def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400952 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700953 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700954 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700955 help='Number of shards to trigger and collect.')
956 parser.add_option_group(parser.sharding_group)
957
958
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400959def add_trigger_options(parser):
960 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500961 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400962 add_filter_options(parser)
963
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400964 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800965 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700966 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500967 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800968 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500969 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700970 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800972 '--env-prefix', default=[], action='append', nargs=2,
973 metavar='VAR local/path',
974 help='Prepend task-relative `local/path` to the task\'s VAR environment '
975 'variable using os-appropriate pathsep character. Can be specified '
976 'multiple times for the same VAR to add multiple paths.')
977 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400978 '--idempotent', action='store_true', default=False,
979 help='When set, the server will actively try to find a previous task '
980 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800981 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700982 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700983 help='The optional path to a file containing the secret_bytes to use with'
984 'this task.')
maruel681d6802017-01-17 16:56:03 -0800985 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700986 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400987 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800988 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700989 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400990 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800991 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500992 '--raw-cmd', action='store_true', default=False,
993 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700994 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800995 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500996 '--relative-cwd',
997 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
998 'requires --raw-cmd')
999 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001000 '--cipd-package', action='append', default=[], metavar='PKG',
1001 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -07001002 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001003 group.add_option(
1004 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -07001005 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001006 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1007 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001008 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001009 help='Email of a service account to run the task as, or literal "bot" '
1010 'string to indicate that the task should use the same account the '
1011 'bot itself is using to authenticate to Swarming. Don\'t use task '
1012 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001013 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001014 '--pool-task-template',
1015 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1016 default='AUTO',
1017 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
1018 'By default, the pool\'s TaskTemplate is automatically selected, '
1019 'according the pool configuration on the server. Choices are: '
1020 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
1021 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001022 '-o', '--output', action='append', default=[], metavar='PATH',
1023 help='A list of files to return in addition to those written to '
1024 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1025 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001026 group.add_option(
1027 '--wait-for-capacity', action='store_true', default=False,
1028 help='Instructs to leave the task PENDING even if there\'s no known bot '
1029 'that could run this task, otherwise the task will be denied with '
1030 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001031 parser.add_option_group(group)
1032
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001033 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001034 group.add_option(
1035 '--priority', type='int', default=100,
1036 help='The lower value, the more important the task is')
1037 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001038 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001039 help='Display name of the task. Defaults to '
1040 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1041 'isolated file is provided, if a hash is provided, it defaults to '
1042 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1043 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001044 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001045 help='Tags to assign to the task.')
1046 group.add_option(
1047 '--user', default='',
1048 help='User associated with the task. Defaults to authenticated user on '
1049 'the server.')
1050 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001051 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001052 help='Seconds to allow the task to be pending for a bot to run before '
1053 'this task request expires.')
1054 group.add_option(
1055 '--deadline', type='int', dest='expiration',
1056 help=optparse.SUPPRESS_HELP)
1057 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001058
1059
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001060def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001061 """Processes trigger options and does preparatory steps.
1062
1063 Returns:
1064 NewTaskRequest instance.
1065 """
maruelaf6b06c2017-06-08 06:26:53 -07001066 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001067 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001068 if args and args[0] == '--':
1069 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001070
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001071 if not options.dimensions:
1072 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001073 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1074 parser.error('--tags must be in the format key:value')
1075 if options.raw_cmd and not args:
1076 parser.error(
1077 'Arguments with --raw-cmd should be passed after -- as command '
1078 'delimiter.')
1079 if options.isolate_server and not options.namespace:
1080 parser.error(
1081 '--namespace must be a valid value when --isolate-server is used')
1082 if not options.isolated and not options.raw_cmd:
1083 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1084
1085 # Isolated
1086 # --isolated is required only if --raw-cmd wasn't provided.
1087 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1088 # preferred server.
1089 isolateserver.process_isolate_server_options(
1090 parser, options, False, not options.raw_cmd)
1091 inputs_ref = None
1092 if options.isolate_server:
1093 inputs_ref = FilesRef(
1094 isolated=options.isolated,
1095 isolatedserver=options.isolate_server,
1096 namespace=options.namespace)
1097
1098 # Command
1099 command = None
1100 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001101 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001102 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001103 if options.relative_cwd:
1104 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1105 if not a.startswith(os.getcwd()):
1106 parser.error(
1107 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001108 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001109 if options.relative_cwd:
1110 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001111 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001112
maruel0a25f6c2017-05-10 10:43:23 -07001113 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001114 cipd_packages = []
1115 for p in options.cipd_package:
1116 split = p.split(':', 2)
1117 if len(split) != 3:
1118 parser.error('CIPD packages must take the form: path:package:version')
1119 cipd_packages.append(CipdPackage(
1120 package_name=split[1],
1121 path=split[0],
1122 version=split[2]))
1123 cipd_input = None
1124 if cipd_packages:
1125 cipd_input = CipdInput(
1126 client_package=None,
1127 packages=cipd_packages,
1128 server=None)
1129
maruel0a25f6c2017-05-10 10:43:23 -07001130 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001131 secret_bytes = None
1132 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001133 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001134 secret_bytes = f.read().encode('base64')
1135
maruel0a25f6c2017-05-10 10:43:23 -07001136 # Named caches
maruel681d6802017-01-17 16:56:03 -08001137 caches = [
1138 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1139 for i in options.named_cache
1140 ]
maruel0a25f6c2017-05-10 10:43:23 -07001141
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001142 env_prefixes = {}
1143 for k, v in options.env_prefix:
1144 env_prefixes.setdefault(k, []).append(v)
1145
maruel77f720b2015-09-15 12:35:22 -07001146 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001147 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001148 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001149 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001150 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001151 dimensions=options.dimensions,
1152 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001153 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001154 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001155 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001156 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001157 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001158 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001159 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001160 outputs=options.output,
1161 secret_bytes=secret_bytes)
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001162 task_slice = TaskSlice(
1163 expiration_secs=options.expiration,
1164 properties=properties,
1165 wait_for_capacity=options.wait_for_capacity)
maruel77f720b2015-09-15 12:35:22 -07001166 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001167 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001168 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001169 priority=options.priority,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001170 task_slices=[task_slice],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001171 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001172 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001173 user=options.user,
1174 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001175
1176
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001177class TaskOutputStdoutOption(optparse.Option):
1178 """Where to output the each task's console output (stderr/stdout).
1179
1180 The output will be;
1181 none - not be downloaded.
1182 json - stored in summary.json file *only*.
1183 console - shown on stdout *only*.
1184 all - stored in summary.json and shown on stdout.
1185 """
1186
1187 choices = ['all', 'json', 'console', 'none']
1188
1189 def __init__(self, *args, **kw):
1190 optparse.Option.__init__(
1191 self,
1192 *args,
1193 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001194 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001195 help=re.sub('\s\s*', ' ', self.__doc__),
1196 **kw)
1197
1198 def convert_value(self, opt, value):
1199 if value not in self.choices:
1200 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1201 self.get_opt_string(), self.choices, value))
1202 stdout_to = []
1203 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001204 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001205 elif value != 'none':
1206 stdout_to = [value]
1207 return stdout_to
1208
1209
maruel@chromium.org0437a732013-08-27 16:05:52 +00001210def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001211 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001212 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001213 help='Timeout to wait for result, set to -1 for no timeout and get '
1214 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001215 parser.group_logging.add_option(
1216 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001217 parser.group_logging.add_option(
1218 '--print-status-updates', action='store_true',
1219 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001220 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001221 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001222 '--task-summary-json',
1223 metavar='FILE',
1224 help='Dump a summary of task results to this file as json. It contains '
1225 'only shards statuses as know to server directly. Any output files '
1226 'emitted by the task can be collected by using --task-output-dir')
1227 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001228 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001229 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001230 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001231 'directory contains per-shard directory with output files produced '
1232 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001233 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001234 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001235 parser.task_output_group.add_option(
1236 '--perf', action='store_true', default=False,
1237 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001238 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001239
1240
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001241def process_collect_options(parser, options):
1242 # Only negative -1 is allowed, disallow other negative values.
1243 if options.timeout != -1 and options.timeout < 0:
1244 parser.error('Invalid --timeout value')
1245
1246
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001247@subcommand.usage('bots...')
1248def CMDbot_delete(parser, args):
1249 """Forcibly deletes bots from the Swarming server."""
1250 parser.add_option(
1251 '-f', '--force', action='store_true',
1252 help='Do not prompt for confirmation')
1253 options, args = parser.parse_args(args)
1254 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001255 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001256
1257 bots = sorted(args)
1258 if not options.force:
1259 print('Delete the following bots?')
1260 for bot in bots:
1261 print(' %s' % bot)
1262 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1263 print('Goodbye.')
1264 return 1
1265
1266 result = 0
1267 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001268 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001269 if net.url_read_json(url, data={}, method='POST') is None:
1270 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001271 result = 1
1272 return result
1273
1274
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001275def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001276 """Returns information about the bots connected to the Swarming server."""
1277 add_filter_options(parser)
1278 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001279 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001280 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001281 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001282 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001283 help='Keep both dead and alive bots')
1284 parser.filter_group.add_option(
1285 '--busy', action='store_true', help='Keep only busy bots')
1286 parser.filter_group.add_option(
1287 '--idle', action='store_true', help='Keep only idle bots')
1288 parser.filter_group.add_option(
1289 '--mp', action='store_true',
1290 help='Keep only Machine Provider managed bots')
1291 parser.filter_group.add_option(
1292 '--non-mp', action='store_true',
1293 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001294 parser.filter_group.add_option(
1295 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001296 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001297 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001298 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001299
1300 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001301 parser.error('Use only one of --keep-dead or --dead-only')
1302 if options.busy and options.idle:
1303 parser.error('Use only one of --busy or --idle')
1304 if options.mp and options.non_mp:
1305 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001306
smut281c3902018-05-30 17:50:05 -07001307 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001308 values = []
1309 if options.dead_only:
1310 values.append(('is_dead', 'TRUE'))
1311 elif options.keep_dead:
1312 values.append(('is_dead', 'NONE'))
1313 else:
1314 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001315
maruelaf6b06c2017-06-08 06:26:53 -07001316 if options.busy:
1317 values.append(('is_busy', 'TRUE'))
1318 elif options.idle:
1319 values.append(('is_busy', 'FALSE'))
1320 else:
1321 values.append(('is_busy', 'NONE'))
1322
1323 if options.mp:
1324 values.append(('is_mp', 'TRUE'))
1325 elif options.non_mp:
1326 values.append(('is_mp', 'FALSE'))
1327 else:
1328 values.append(('is_mp', 'NONE'))
1329
1330 for key, value in options.dimensions:
1331 values.append(('dimensions', '%s:%s' % (key, value)))
1332 url += urllib.urlencode(values)
1333 try:
1334 data, yielder = get_yielder(url, 0)
1335 bots = data.get('items') or []
1336 for items in yielder():
1337 if items:
1338 bots.extend(items)
1339 except Failure as e:
1340 sys.stderr.write('\n%s\n' % e)
1341 return 1
maruel77f720b2015-09-15 12:35:22 -07001342 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001343 print bot['bot_id']
1344 if not options.bare:
1345 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1346 print ' %s' % json.dumps(dimensions, sort_keys=True)
1347 if bot.get('task_id'):
1348 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001349 return 0
1350
1351
maruelfd0a90c2016-06-10 11:51:10 -07001352@subcommand.usage('task_id')
1353def CMDcancel(parser, args):
1354 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001355 parser.add_option(
1356 '-k', '--kill-running', action='store_true', default=False,
1357 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001358 options, args = parser.parse_args(args)
1359 if not args:
1360 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001361 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001362 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001363 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001364 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001365 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001366 print('Deleting %s failed. Probably already gone' % task_id)
1367 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001368 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001369 return 0
1370
1371
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001372@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001373def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001374 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001375
1376 The result can be in multiple part if the execution was sharded. It can
1377 potentially have retries.
1378 """
1379 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001380 parser.add_option(
1381 '-j', '--json',
1382 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001383 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001384 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001385 if not args and not options.json:
1386 parser.error('Must specify at least one task id or --json.')
1387 if args and options.json:
1388 parser.error('Only use one of task id or --json.')
1389
1390 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001391 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001392 try:
maruel1ceb3872015-10-14 06:10:44 -07001393 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001394 data = json.load(f)
1395 except (IOError, ValueError):
1396 parser.error('Failed to open %s' % options.json)
1397 try:
1398 tasks = sorted(
1399 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1400 args = [t['task_id'] for t in tasks]
1401 except (KeyError, TypeError):
1402 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001403 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001404 # Take in account all the task slices.
1405 offset = 0
1406 for s in data['request']['task_slices']:
1407 m = (offset + s['properties']['execution_timeout_secs'] +
1408 s['expiration_secs'])
1409 if m > options.timeout:
1410 options.timeout = m
1411 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001412 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001413 else:
1414 valid = frozenset('0123456789abcdef')
1415 if any(not valid.issuperset(task_id) for task_id in args):
1416 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001417
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001418 try:
1419 return collect(
1420 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001421 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001422 options.timeout,
1423 options.decorate,
1424 options.print_status_updates,
1425 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001426 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001427 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001428 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001429 except Failure:
1430 on_error.report(None)
1431 return 1
1432
1433
maruel77f720b2015-09-15 12:35:22 -07001434@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001435def CMDpost(parser, args):
1436 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1437
1438 Input data must be sent to stdin, result is printed to stdout.
1439
1440 If HTTP response code >= 400, returns non-zero.
1441 """
1442 options, args = parser.parse_args(args)
1443 if len(args) != 1:
1444 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001445 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001446 data = sys.stdin.read()
1447 try:
1448 resp = net.url_read(url, data=data, method='POST')
1449 except net.TimeoutError:
1450 sys.stderr.write('Timeout!\n')
1451 return 1
1452 if not resp:
1453 sys.stderr.write('No response!\n')
1454 return 1
1455 sys.stdout.write(resp)
1456 return 0
1457
1458
1459@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001460def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001461 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1462 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001463
1464 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001465 Raw task request and results:
1466 swarming.py query -S server-url.com task/123456/request
1467 swarming.py query -S server-url.com task/123456/result
1468
maruel77f720b2015-09-15 12:35:22 -07001469 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001470 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001471
maruelaf6b06c2017-06-08 06:26:53 -07001472 Listing last 10 tasks on a specific bot named 'bot1':
1473 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001474
maruelaf6b06c2017-06-08 06:26:53 -07001475 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001476 quoting is important!:
1477 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001478 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001479 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001480 parser.add_option(
1481 '-L', '--limit', type='int', default=200,
1482 help='Limit to enforce on limitless items (like number of tasks); '
1483 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001484 parser.add_option(
1485 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001486 parser.add_option(
1487 '--progress', action='store_true',
1488 help='Prints a dot at each request to show progress')
1489 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001490 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001491 parser.error(
1492 'Must specify only method name and optionally query args properly '
1493 'escaped.')
smut281c3902018-05-30 17:50:05 -07001494 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001495 try:
1496 data, yielder = get_yielder(base_url, options.limit)
1497 for items in yielder():
1498 if items:
1499 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001500 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001501 sys.stderr.write('.')
1502 sys.stderr.flush()
1503 except Failure as e:
1504 sys.stderr.write('\n%s\n' % e)
1505 return 1
maruel77f720b2015-09-15 12:35:22 -07001506 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001507 sys.stderr.write('\n')
1508 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001509 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001510 options.json = unicode(os.path.abspath(options.json))
1511 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001512 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001513 try:
maruel77f720b2015-09-15 12:35:22 -07001514 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001515 sys.stdout.write('\n')
1516 except IOError:
1517 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001518 return 0
1519
1520
maruel77f720b2015-09-15 12:35:22 -07001521def CMDquery_list(parser, args):
1522 """Returns list of all the Swarming APIs that can be used with command
1523 'query'.
1524 """
1525 parser.add_option(
1526 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1527 options, args = parser.parse_args(args)
1528 if args:
1529 parser.error('No argument allowed.')
1530
1531 try:
1532 apis = endpoints_api_discovery_apis(options.swarming)
1533 except APIError as e:
1534 parser.error(str(e))
1535 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001536 options.json = unicode(os.path.abspath(options.json))
1537 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001538 json.dump(apis, f)
1539 else:
1540 help_url = (
1541 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1542 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001543 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1544 if i:
1545 print('')
maruel77f720b2015-09-15 12:35:22 -07001546 print api_id
maruel11e31af2017-02-15 07:30:50 -08001547 print ' ' + api['description'].strip()
1548 if 'resources' in api:
1549 # Old.
1550 for j, (resource_name, resource) in enumerate(
1551 sorted(api['resources'].iteritems())):
1552 if j:
1553 print('')
1554 for method_name, method in sorted(resource['methods'].iteritems()):
1555 # Only list the GET ones.
1556 if method['httpMethod'] != 'GET':
1557 continue
1558 print '- %s.%s: %s' % (
1559 resource_name, method_name, method['path'])
1560 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001561 ' ' + l for l in textwrap.wrap(
1562 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001563 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1564 else:
1565 # New.
1566 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001567 # Only list the GET ones.
1568 if method['httpMethod'] != 'GET':
1569 continue
maruel11e31af2017-02-15 07:30:50 -08001570 print '- %s: %s' % (method['id'], method['path'])
1571 print('\n'.join(
1572 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001573 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1574 return 0
1575
1576
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001577@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001578def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001579 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001580
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001581 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001582 """
1583 add_trigger_options(parser)
1584 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001585 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001586 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001587 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001588 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001589 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001590 tasks = trigger_task_shards(
1591 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001592 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001593 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001594 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001595 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001596 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001597 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001598 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001599 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001600 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001601 task_ids = [
1602 t['task_id']
1603 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1604 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001605 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001606 offset = 0
1607 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001608 m = (offset + s.properties.execution_timeout_secs +
1609 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001610 if m > options.timeout:
1611 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001612 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001613 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001614 try:
1615 return collect(
1616 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001617 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001618 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001619 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001620 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001621 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001622 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001623 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001624 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001625 except Failure:
1626 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001627 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001628
1629
maruel18122c62015-10-23 06:31:23 -07001630@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001631def CMDreproduce(parser, args):
1632 """Runs a task locally that was triggered on the server.
1633
1634 This running locally the same commands that have been run on the bot. The data
1635 downloaded will be in a subdirectory named 'work' of the current working
1636 directory.
maruel18122c62015-10-23 06:31:23 -07001637
1638 You can pass further additional arguments to the target command by passing
1639 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001640 """
maruelc070e672016-02-22 17:32:57 -08001641 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001642 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001643 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001644 parser.add_option(
1645 '--work', metavar='DIR', default='work',
1646 help='Directory to map the task input files into')
1647 parser.add_option(
1648 '--cache', metavar='DIR', default='cache',
1649 help='Directory that contains the input cache')
1650 parser.add_option(
1651 '--leak', action='store_true',
1652 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001653 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001654 extra_args = []
1655 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001656 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001657 if len(args) > 1:
1658 if args[1] == '--':
1659 if len(args) > 2:
1660 extra_args = args[2:]
1661 else:
1662 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001663
smut281c3902018-05-30 17:50:05 -07001664 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001665 request = net.url_read_json(url)
1666 if not request:
1667 print >> sys.stderr, 'Failed to retrieve request data for the task'
1668 return 1
1669
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001670 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001671 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001672 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001673 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001674 cachedir = unicode(os.path.abspath('cipd_cache'))
1675 if not fs.exists(cachedir):
1676 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001677
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001678 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001679 env = os.environ.copy()
1680 env['SWARMING_BOT_ID'] = 'reproduce'
1681 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001682 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001683 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001684 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001685 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001686 if not i['value']:
1687 env.pop(key, None)
1688 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001689 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001690
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001691 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001692 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001693 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001694 for i in env_prefixes:
1695 key = i['key']
1696 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001697 cur = env.get(key)
1698 if cur:
1699 paths.append(cur)
1700 env[key] = os.path.pathsep.join(paths)
1701
iannucci31ab9192017-05-02 19:11:56 -07001702 command = []
nodir152cba62016-05-12 16:08:56 -07001703 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001704 # Create the tree.
1705 with isolateserver.get_storage(
1706 properties['inputs_ref']['isolatedserver'],
1707 properties['inputs_ref']['namespace']) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001708 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1709 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1710 # leak.
1711 policies = local_caching.CachePolicies(0, 0, 0, 0)
1712 algo = isolated_format.get_hash_algo(
1713 properties['inputs_ref']['namespace'])
1714 cache = local_caching.DiskContentAddressedCache(
1715 unicode(os.path.abspath(options.cache)), policies, algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001716 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001717 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001718 command = bundle.command
1719 if bundle.relative_cwd:
1720 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001721 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001722
1723 if properties.get('command'):
1724 command.extend(properties['command'])
1725
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001726 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001727 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001728 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001729 new_command = run_isolated.process_command(command, 'invalid', None)
1730 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001731 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001732 else:
1733 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001734 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001735 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001736 command, options.output, None)
1737 if not os.path.isdir(options.output):
1738 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001739 command = new_command
1740 file_path.ensure_command_has_abs_path(command, workdir)
1741
1742 if properties.get('cipd_input'):
1743 ci = properties['cipd_input']
1744 cp = ci['client_package']
1745 client_manager = cipd.get_client(
1746 ci['server'], cp['package_name'], cp['version'], cachedir)
1747
1748 with client_manager as client:
1749 by_path = collections.defaultdict(list)
1750 for pkg in ci['packages']:
1751 path = pkg['path']
1752 # cipd deals with 'root' as ''
1753 if path == '.':
1754 path = ''
1755 by_path[path].append((pkg['package_name'], pkg['version']))
1756 client.ensure(workdir, by_path, cache_dir=cachedir)
1757
maruel77f720b2015-09-15 12:35:22 -07001758 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001759 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001760 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001761 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001762 print >> sys.stderr, str(e)
1763 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001764 finally:
1765 # Do not delete options.cache.
1766 if not options.leak:
1767 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001768
1769
maruel0eb1d1b2015-10-02 14:48:21 -07001770@subcommand.usage('bot_id')
1771def CMDterminate(parser, args):
1772 """Tells a bot to gracefully shut itself down as soon as it can.
1773
1774 This is done by completing whatever current task there is then exiting the bot
1775 process.
1776 """
1777 parser.add_option(
1778 '--wait', action='store_true', help='Wait for the bot to terminate')
1779 options, args = parser.parse_args(args)
1780 if len(args) != 1:
1781 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001782 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001783 request = net.url_read_json(url, data={})
1784 if not request:
1785 print >> sys.stderr, 'Failed to ask for termination'
1786 return 1
1787 if options.wait:
1788 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001789 options.swarming,
1790 [request['task_id']],
1791 0.,
1792 False,
1793 False,
1794 None,
1795 None,
1796 [],
maruel9531ce02016-04-13 06:11:23 -07001797 False)
maruelbfc5f872017-06-10 16:43:17 -07001798 else:
1799 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001800 return 0
1801
1802
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001803@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001804def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001805 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001806
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001807 Passes all extra arguments provided after '--' as additional command line
1808 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001809 """
1810 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001811 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001812 parser.add_option(
1813 '--dump-json',
1814 metavar='FILE',
1815 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001816 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001817 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001818 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001819 tasks = trigger_task_shards(
1820 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001821 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001822 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001823 tasks_sorted = sorted(
1824 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001825 if options.dump_json:
1826 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001827 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001828 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001829 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001830 }
maruel46b015f2015-10-13 18:40:35 -07001831 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001832 print('To collect results, use:')
1833 print(' swarming.py collect -S %s --json %s' %
1834 (options.swarming, options.dump_json))
1835 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001836 print('To collect results, use:')
1837 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001838 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1839 print('Or visit:')
1840 for t in tasks_sorted:
1841 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001842 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001843 except Failure:
1844 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001845 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001846
1847
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001848class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001849 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001850 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001851 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001852 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001853 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001854 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001855 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001856 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001857 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001858 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001859
1860 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001861 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001862 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001863 auth.process_auth_options(self, options)
1864 user = self._process_swarming(options)
1865 if hasattr(options, 'user') and not options.user:
1866 options.user = user
1867 return options, args
1868
1869 def _process_swarming(self, options):
1870 """Processes the --swarming option and aborts if not specified.
1871
1872 Returns the identity as determined by the server.
1873 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001874 if not options.swarming:
1875 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001876 try:
1877 options.swarming = net.fix_url(options.swarming)
1878 except ValueError as e:
1879 self.error('--swarming %s' % e)
1880 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001881 try:
1882 user = auth.ensure_logged_in(options.swarming)
1883 except ValueError as e:
1884 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001885 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001886
1887
1888def main(args):
1889 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001890 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001891
1892
1893if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001894 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001895 fix_encoding.fix_encoding()
1896 tools.disable_buffering()
1897 colorama.init()
1898 sys.exit(main(sys.argv[1:]))