blob: 15e155468fdd55bce5785b027a94572e3fcd65ac [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 Ruel20b764d2018-06-22 18:08:37 +0000291class TaskState(object):
292 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000293
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000294 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
295 is the source of truth for these values:
296 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400297
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000298 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400299 """
300 RUNNING = 0x10
301 PENDING = 0x20
302 EXPIRED = 0x30
303 TIMED_OUT = 0x40
304 BOT_DIED = 0x50
305 CANCELED = 0x60
306 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400307 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400308 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400309
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000310 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400311
maruel77f720b2015-09-15 12:35:22 -0700312 _ENUMS = {
313 'RUNNING': RUNNING,
314 'PENDING': PENDING,
315 'EXPIRED': EXPIRED,
316 'TIMED_OUT': TIMED_OUT,
317 'BOT_DIED': BOT_DIED,
318 'CANCELED': CANCELED,
319 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400320 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400321 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700322 }
323
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400324 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700325 def from_enum(cls, state):
326 """Returns int value based on the string."""
327 if state not in cls._ENUMS:
328 raise ValueError('Invalid state %s' % state)
329 return cls._ENUMS[state]
330
maruel@chromium.org0437a732013-08-27 16:05:52 +0000331
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700332class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700333 """Assembles task execution summary (for --task-summary-json output).
334
335 Optionally fetches task outputs from isolate server to local disk (used when
336 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700337
338 This object is shared among multiple threads running 'retrieve_results'
339 function, in particular they call 'process_shard_result' method in parallel.
340 """
341
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000342 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700343 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
344
345 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700346 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700347 shard_count: expected number of task shards.
348 """
maruel12e30012015-10-09 11:55:35 -0700349 self.task_output_dir = (
350 unicode(os.path.abspath(task_output_dir))
351 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000352 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353 self.shard_count = shard_count
354
355 self._lock = threading.Lock()
356 self._per_shard_results = {}
357 self._storage = None
358
nodire5028a92016-04-29 14:38:21 -0700359 if self.task_output_dir:
360 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361
Vadim Shtayurab450c602014-05-12 19:23:25 -0700362 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 """Stores results of a single task shard, fetches output files if necessary.
364
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400365 Modifies |result| in place.
366
maruel77f720b2015-09-15 12:35:22 -0700367 shard_index is 0-based.
368
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369 Called concurrently from multiple threads.
370 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700371 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700372 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700373 if shard_index < 0 or shard_index >= self.shard_count:
374 logging.warning(
375 'Shard index %d is outside of expected range: [0; %d]',
376 shard_index, self.shard_count - 1)
377 return
378
maruel77f720b2015-09-15 12:35:22 -0700379 if result.get('outputs_ref'):
380 ref = result['outputs_ref']
381 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
382 ref['isolatedserver'],
383 urllib.urlencode(
384 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400385
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 # Store result dict of that shard, ignore results we've already seen.
387 with self._lock:
388 if shard_index in self._per_shard_results:
389 logging.warning('Ignoring duplicate shard index %d', shard_index)
390 return
391 self._per_shard_results[shard_index] = result
392
393 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700394 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400395 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700396 result['outputs_ref']['isolatedserver'],
397 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400398 if storage:
399 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400400 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
401 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400402 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700403 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400404 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400405 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700406 os.path.join(self.task_output_dir, str(shard_index)),
407 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700408
409 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700410 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700411 with self._lock:
412 # Write an array of shard results with None for missing shards.
413 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700414 'shards': [
415 self._per_shard_results.get(i) for i in xrange(self.shard_count)
416 ],
417 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000418
419 # Don't store stdout in the summary if not requested too.
420 if "json" not in self.task_output_stdout:
421 for shard_json in summary['shards']:
422 if not shard_json:
423 continue
424 if "output" in shard_json:
425 del shard_json["output"]
426 if "outputs" in shard_json:
427 del shard_json["outputs"]
428
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700429 # Write summary.json to task_output_dir as well.
430 if self.task_output_dir:
431 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700432 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700433 summary,
434 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700435 if self._storage:
436 self._storage.close()
437 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700438 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439
440 def _get_storage(self, isolate_server, namespace):
441 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 with self._lock:
444 if not self._storage:
445 self._storage = isolateserver.get_storage(isolate_server, namespace)
446 else:
447 # Shards must all use exact same isolate server and namespace.
448 if self._storage.location != isolate_server:
449 logging.error(
450 'Task shards are using multiple isolate servers: %s and %s',
451 self._storage.location, isolate_server)
452 return None
453 if self._storage.namespace != namespace:
454 logging.error(
455 'Task shards are using multiple namespaces: %s and %s',
456 self._storage.namespace, namespace)
457 return None
458 return self._storage
459
460
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500461def now():
462 """Exists so it can be mocked easily."""
463 return time.time()
464
465
maruel77f720b2015-09-15 12:35:22 -0700466def parse_time(value):
467 """Converts serialized time from the API to datetime.datetime."""
468 # When microseconds are 0, the '.123456' suffix is elided. This means the
469 # serialized format is not consistent, which confuses the hell out of python.
470 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
471 try:
472 return datetime.datetime.strptime(value, fmt)
473 except ValueError:
474 pass
475 raise ValueError('Failed to parse %s' % value)
476
477
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700478def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700479 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000480 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400481 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700482
Vadim Shtayurab450c602014-05-12 19:23:25 -0700483 Returns:
484 <result dict> on success.
485 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700486 """
maruel71c61c82016-02-22 06:52:05 -0800487 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700488 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700489 if include_perf:
490 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700491 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700492 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400493 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494 attempt = 0
495
496 while not should_stop.is_set():
497 attempt += 1
498
499 # Waiting for too long -> give up.
500 current_time = now()
501 if deadline and current_time >= deadline:
502 logging.error('retrieve_results(%s) timed out on attempt %d',
503 base_url, attempt)
504 return None
505
506 # Do not spin too fast. Spin faster at the beginning though.
507 # Start with 1 sec delay and for each 30 sec of waiting add another second
508 # of delay, until hitting 15 sec ceiling.
509 if attempt > 1:
510 max_delay = min(15, 1 + (current_time - started) / 30.0)
511 delay = min(max_delay, deadline - current_time) if deadline else max_delay
512 if delay > 0:
513 logging.debug('Waiting %.1f sec before retrying', delay)
514 should_stop.wait(delay)
515 if should_stop.is_set():
516 return None
517
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400518 # Disable internal retries in net.url_read_json, since we are doing retries
519 # ourselves.
520 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700521 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
522 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400523 # Retry on 500s only if no timeout is specified.
524 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400525 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400526 if timeout == -1:
527 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400528 continue
maruel77f720b2015-09-15 12:35:22 -0700529
maruelbf53e042015-12-01 15:00:51 -0800530 if result.get('error'):
531 # An error occurred.
532 if result['error'].get('errors'):
533 for err in result['error']['errors']:
534 logging.warning(
535 'Error while reading task: %s; %s',
536 err.get('message'), err.get('debugInfo'))
537 elif result['error'].get('message'):
538 logging.warning(
539 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400540 if timeout == -1:
541 return result
maruelbf53e042015-12-01 15:00:51 -0800542 continue
543
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400544 # When timeout == -1, always return on first attempt. 500s are already
545 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000546 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000547 if fetch_stdout:
548 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700549 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700550 # Record the result, try to fetch attached output files (if any).
551 if output_collector:
552 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700553 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700554 if result.get('internal_failure'):
555 logging.error('Internal error!')
556 elif result['state'] == 'BOT_DIED':
557 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700558 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000559
560
maruel77f720b2015-09-15 12:35:22 -0700561def convert_to_old_format(result):
562 """Converts the task result data from Endpoints API format to old API format
563 for compatibility.
564
565 This goes into the file generated as --task-summary-json.
566 """
567 # Sets default.
568 result.setdefault('abandoned_ts', None)
569 result.setdefault('bot_id', None)
570 result.setdefault('bot_version', None)
571 result.setdefault('children_task_ids', [])
572 result.setdefault('completed_ts', None)
573 result.setdefault('cost_saved_usd', None)
574 result.setdefault('costs_usd', None)
575 result.setdefault('deduped_from', None)
576 result.setdefault('name', None)
577 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700578 result.setdefault('server_versions', None)
579 result.setdefault('started_ts', None)
580 result.setdefault('tags', None)
581 result.setdefault('user', None)
582
583 # Convertion back to old API.
584 duration = result.pop('duration', None)
585 result['durations'] = [duration] if duration else []
586 exit_code = result.pop('exit_code', None)
587 result['exit_codes'] = [int(exit_code)] if exit_code else []
588 result['id'] = result.pop('task_id')
589 result['isolated_out'] = result.get('outputs_ref', None)
590 output = result.pop('output', None)
591 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700592 # server_version
593 # Endpoints result 'state' as string. For compatibility with old code, convert
594 # to int.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000595 result['state'] = TaskState.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700596 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700597 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700598 if 'bot_dimensions' in result:
599 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700600 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700601 }
602 else:
603 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700604
605
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700606def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400607 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000608 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500609 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000610
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700611 Duplicate shards are ignored. Shards are yielded in order of completion.
612 Timed out shards are NOT yielded at all. Caller can compare number of yielded
613 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000614
615 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500616 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 +0000617 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500618
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700619 output_collector is an optional instance of TaskOutputCollector that will be
620 used to fetch files produced by a task from isolate server to the local disk.
621
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500622 Yields:
623 (index, result). In particular, 'result' is defined as the
624 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000626 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400627 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700628 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700629 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700630
maruel@chromium.org0437a732013-08-27 16:05:52 +0000631 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
632 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 # Adds a task to the thread pool to call 'retrieve_results' and return
634 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400635 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000636 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700637 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000638 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400639 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000640 task_id, timeout, should_stop, output_collector, include_perf,
641 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700642
643 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400644 for shard_index, task_id in enumerate(task_ids):
645 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700646
647 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400648 shards_remaining = range(len(task_ids))
649 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700650 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700651 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700652 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700653 shard_index, result = results_channel.pull(
654 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700655 except threading_utils.TaskChannel.Timeout:
656 if print_status_updates:
657 print(
658 'Waiting for results from the following shards: %s' %
659 ', '.join(map(str, shards_remaining)))
660 sys.stdout.flush()
661 continue
662 except Exception:
663 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700664
665 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700666 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000667 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500668 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000669 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700670
Vadim Shtayurab450c602014-05-12 19:23:25 -0700671 # Yield back results to the caller.
672 assert shard_index in shards_remaining
673 shards_remaining.remove(shard_index)
674 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700675
maruel@chromium.org0437a732013-08-27 16:05:52 +0000676 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700677 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000678 should_stop.set()
679
680
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000681def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000682 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700683 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400684 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700685 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
686 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400687 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
688 metadata.get('abandoned_ts')):
689 pending = '%.1fs' % (
690 parse_time(metadata['abandoned_ts']) -
691 parse_time(metadata['created_ts'])
692 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400693 else:
694 pending = 'N/A'
695
maruel77f720b2015-09-15 12:35:22 -0700696 if metadata.get('duration') is not None:
697 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400698 else:
699 duration = 'N/A'
700
maruel77f720b2015-09-15 12:35:22 -0700701 if metadata.get('exit_code') is not None:
702 # Integers are encoded as string to not loose precision.
703 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400704 else:
705 exit_code = 'N/A'
706
707 bot_id = metadata.get('bot_id') or 'N/A'
708
maruel77f720b2015-09-15 12:35:22 -0700709 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400710 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000711 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400712 if metadata.get('state') == 'CANCELED':
713 tag_footer2 = ' Pending: %s CANCELED' % pending
714 elif metadata.get('state') == 'EXPIRED':
715 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400716 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400717 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
718 pending, duration, bot_id, exit_code, metadata['state'])
719 else:
720 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
721 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400722
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000723 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
724 dash_pad = '+-%s-+' % ('-' * tag_len)
725 tag_header = '| %s |' % tag_header.ljust(tag_len)
726 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
727 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400728
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000729 if include_stdout:
730 return '\n'.join([
731 dash_pad,
732 tag_header,
733 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400734 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000735 dash_pad,
736 tag_footer1,
737 tag_footer2,
738 dash_pad,
739 ])
740 else:
741 return '\n'.join([
742 dash_pad,
743 tag_header,
744 tag_footer2,
745 dash_pad,
746 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000747
748
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700749def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700750 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000751 task_summary_json, task_output_dir, task_output_stdout,
752 include_perf):
maruela5490782015-09-30 10:56:59 -0700753 """Retrieves results of a Swarming task.
754
755 Returns:
756 process exit code that should be returned to the user.
757 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700758 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000759 output_collector = TaskOutputCollector(
760 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700761
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700762 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700763 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400764 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700765 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400766 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400767 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000768 output_collector, include_perf,
769 (len(task_output_stdout) > 0),
770 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700771 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700772
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400773 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700774 shard_exit_code = metadata.get('exit_code')
775 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700776 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700777 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700778 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400779 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700780 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700781
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000783 s = decorate_shard_output(
784 swarming, index, metadata,
785 "console" in task_output_stdout).encode(
786 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700787 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400788 if len(seen_shards) < len(task_ids):
789 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790 else:
maruel77f720b2015-09-15 12:35:22 -0700791 print('%s: %s %s' % (
792 metadata.get('bot_id', 'N/A'),
793 metadata['task_id'],
794 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000795 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700796 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400797 if output:
798 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700799 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700800 summary = output_collector.finalize()
801 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700802 # TODO(maruel): Make this optional.
803 for i in summary['shards']:
804 if i:
805 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700806 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700807
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400808 if decorate and total_duration:
809 print('Total duration: %.1fs' % total_duration)
810
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400811 if len(seen_shards) != len(task_ids):
812 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700813 print >> sys.stderr, ('Results from some shards are missing: %s' %
814 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700815 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700816
maruela5490782015-09-30 10:56:59 -0700817 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000818
819
maruel77f720b2015-09-15 12:35:22 -0700820### API management.
821
822
823class APIError(Exception):
824 pass
825
826
827def endpoints_api_discovery_apis(host):
828 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
829 the APIs exposed by a host.
830
831 https://developers.google.com/discovery/v1/reference/apis/list
832 """
maruel380e3262016-08-31 16:10:06 -0700833 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
834 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700835 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
836 if data is None:
837 raise APIError('Failed to discover APIs on %s' % host)
838 out = {}
839 for api in data['items']:
840 if api['id'] == 'discovery:v1':
841 continue
842 # URL is of the following form:
843 # url = host + (
844 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
845 api_data = net.url_read_json(api['discoveryRestUrl'])
846 if api_data is None:
847 raise APIError('Failed to discover %s on %s' % (api['id'], host))
848 out[api['id']] = api_data
849 return out
850
851
maruelaf6b06c2017-06-08 06:26:53 -0700852def get_yielder(base_url, limit):
853 """Returns the first query and a function that yields following items."""
854 CHUNK_SIZE = 250
855
856 url = base_url
857 if limit:
858 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
859 data = net.url_read_json(url)
860 if data is None:
861 # TODO(maruel): Do basic diagnostic.
862 raise Failure('Failed to access %s' % url)
863 org_cursor = data.pop('cursor', None)
864 org_total = len(data.get('items') or [])
865 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
866 if not org_cursor or not org_total:
867 # This is not an iterable resource.
868 return data, lambda: []
869
870 def yielder():
871 cursor = org_cursor
872 total = org_total
873 # Some items support cursors. Try to get automatically if cursors are needed
874 # by looking at the 'cursor' items.
875 while cursor and (not limit or total < limit):
876 merge_char = '&' if '?' in base_url else '?'
877 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
878 if limit:
879 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
880 new = net.url_read_json(url)
881 if new is None:
882 raise Failure('Failed to access %s' % url)
883 cursor = new.get('cursor')
884 new_items = new.get('items')
885 nb_items = len(new_items or [])
886 total += nb_items
887 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
888 yield new_items
889
890 return data, yielder
891
892
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500893### Commands.
894
895
896def abort_task(_swarming, _manifest):
897 """Given a task manifest that was triggered, aborts its execution."""
898 # TODO(vadimsh): No supported by the server yet.
899
900
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400901def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800902 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500903 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500904 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500905 dest='dimensions', metavar='FOO bar',
906 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500907 parser.add_option_group(parser.filter_group)
908
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909
maruelaf6b06c2017-06-08 06:26:53 -0700910def process_filter_options(parser, options):
911 for key, value in options.dimensions:
912 if ':' in key:
913 parser.error('--dimension key cannot contain ":"')
914 if key.strip() != key:
915 parser.error('--dimension key has whitespace')
916 if not key:
917 parser.error('--dimension key is empty')
918
919 if value.strip() != value:
920 parser.error('--dimension value has whitespace')
921 if not value:
922 parser.error('--dimension value is empty')
923 options.dimensions.sort()
924
925
Vadim Shtayurab450c602014-05-12 19:23:25 -0700926def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400927 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700928 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700929 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700930 help='Number of shards to trigger and collect.')
931 parser.add_option_group(parser.sharding_group)
932
933
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400934def add_trigger_options(parser):
935 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500936 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400937 add_filter_options(parser)
938
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400939 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800940 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700941 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500942 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800943 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500944 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700945 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800946 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800947 '--env-prefix', default=[], action='append', nargs=2,
948 metavar='VAR local/path',
949 help='Prepend task-relative `local/path` to the task\'s VAR environment '
950 'variable using os-appropriate pathsep character. Can be specified '
951 'multiple times for the same VAR to add multiple paths.')
952 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400953 '--idempotent', action='store_true', default=False,
954 help='When set, the server will actively try to find a previous task '
955 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800956 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700957 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700958 help='The optional path to a file containing the secret_bytes to use with'
959 'this task.')
maruel681d6802017-01-17 16:56:03 -0800960 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700961 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400962 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800963 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700964 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400965 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800966 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500967 '--raw-cmd', action='store_true', default=False,
968 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700969 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800970 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500971 '--relative-cwd',
972 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
973 'requires --raw-cmd')
974 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700975 '--cipd-package', action='append', default=[], metavar='PKG',
976 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700977 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800978 group.add_option(
979 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700980 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800981 help='"<name> <relpath>" items to keep a persistent bot managed cache')
982 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700983 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700984 help='Email of a service account to run the task as, or literal "bot" '
985 'string to indicate that the task should use the same account the '
986 'bot itself is using to authenticate to Swarming. Don\'t use task '
987 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800988 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +0000989 '--pool-task-template',
990 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
991 default='AUTO',
992 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
993 'By default, the pool\'s TaskTemplate is automatically selected, '
994 'according the pool configuration on the server. Choices are: '
995 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
996 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700997 '-o', '--output', action='append', default=[], metavar='PATH',
998 help='A list of files to return in addition to those written to '
999 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1000 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001001 group.add_option(
1002 '--wait-for-capacity', action='store_true', default=False,
1003 help='Instructs to leave the task PENDING even if there\'s no known bot '
1004 'that could run this task, otherwise the task will be denied with '
1005 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001006 parser.add_option_group(group)
1007
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001008 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001009 group.add_option(
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +00001010 '--priority', type='int', default=200,
maruel681d6802017-01-17 16:56:03 -08001011 help='The lower value, the more important the task is')
1012 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001013 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001014 help='Display name of the task. Defaults to '
1015 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1016 'isolated file is provided, if a hash is provided, it defaults to '
1017 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1018 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001019 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001020 help='Tags to assign to the task.')
1021 group.add_option(
1022 '--user', default='',
1023 help='User associated with the task. Defaults to authenticated user on '
1024 'the server.')
1025 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001026 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001027 help='Seconds to allow the task to be pending for a bot to run before '
1028 'this task request expires.')
1029 group.add_option(
1030 '--deadline', type='int', dest='expiration',
1031 help=optparse.SUPPRESS_HELP)
1032 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001033
1034
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001035def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001036 """Processes trigger options and does preparatory steps.
1037
1038 Returns:
1039 NewTaskRequest instance.
1040 """
maruelaf6b06c2017-06-08 06:26:53 -07001041 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001042 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001043 if args and args[0] == '--':
1044 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001045
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001046 if not options.dimensions:
1047 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001048 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1049 parser.error('--tags must be in the format key:value')
1050 if options.raw_cmd and not args:
1051 parser.error(
1052 'Arguments with --raw-cmd should be passed after -- as command '
1053 'delimiter.')
1054 if options.isolate_server and not options.namespace:
1055 parser.error(
1056 '--namespace must be a valid value when --isolate-server is used')
1057 if not options.isolated and not options.raw_cmd:
1058 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1059
1060 # Isolated
1061 # --isolated is required only if --raw-cmd wasn't provided.
1062 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1063 # preferred server.
1064 isolateserver.process_isolate_server_options(
1065 parser, options, False, not options.raw_cmd)
1066 inputs_ref = None
1067 if options.isolate_server:
1068 inputs_ref = FilesRef(
1069 isolated=options.isolated,
1070 isolatedserver=options.isolate_server,
1071 namespace=options.namespace)
1072
1073 # Command
1074 command = None
1075 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001077 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001078 if options.relative_cwd:
1079 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1080 if not a.startswith(os.getcwd()):
1081 parser.error(
1082 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001083 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001084 if options.relative_cwd:
1085 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001086 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001087
maruel0a25f6c2017-05-10 10:43:23 -07001088 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001089 cipd_packages = []
1090 for p in options.cipd_package:
1091 split = p.split(':', 2)
1092 if len(split) != 3:
1093 parser.error('CIPD packages must take the form: path:package:version')
1094 cipd_packages.append(CipdPackage(
1095 package_name=split[1],
1096 path=split[0],
1097 version=split[2]))
1098 cipd_input = None
1099 if cipd_packages:
1100 cipd_input = CipdInput(
1101 client_package=None,
1102 packages=cipd_packages,
1103 server=None)
1104
maruel0a25f6c2017-05-10 10:43:23 -07001105 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001106 secret_bytes = None
1107 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001108 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001109 secret_bytes = f.read().encode('base64')
1110
maruel0a25f6c2017-05-10 10:43:23 -07001111 # Named caches
maruel681d6802017-01-17 16:56:03 -08001112 caches = [
1113 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1114 for i in options.named_cache
1115 ]
maruel0a25f6c2017-05-10 10:43:23 -07001116
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001117 env_prefixes = {}
1118 for k, v in options.env_prefix:
1119 env_prefixes.setdefault(k, []).append(v)
1120
maruel77f720b2015-09-15 12:35:22 -07001121 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001122 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001123 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001124 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001125 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001126 dimensions=options.dimensions,
1127 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001128 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001129 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001130 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001131 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001132 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001133 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001134 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001135 outputs=options.output,
1136 secret_bytes=secret_bytes)
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001137 task_slice = TaskSlice(
1138 expiration_secs=options.expiration,
1139 properties=properties,
1140 wait_for_capacity=options.wait_for_capacity)
maruel77f720b2015-09-15 12:35:22 -07001141 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001142 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001143 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001144 priority=options.priority,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001145 task_slices=[task_slice],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001146 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001147 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001148 user=options.user,
1149 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001150
1151
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001152class TaskOutputStdoutOption(optparse.Option):
1153 """Where to output the each task's console output (stderr/stdout).
1154
1155 The output will be;
1156 none - not be downloaded.
1157 json - stored in summary.json file *only*.
1158 console - shown on stdout *only*.
1159 all - stored in summary.json and shown on stdout.
1160 """
1161
1162 choices = ['all', 'json', 'console', 'none']
1163
1164 def __init__(self, *args, **kw):
1165 optparse.Option.__init__(
1166 self,
1167 *args,
1168 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001169 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001170 help=re.sub('\s\s*', ' ', self.__doc__),
1171 **kw)
1172
1173 def convert_value(self, opt, value):
1174 if value not in self.choices:
1175 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1176 self.get_opt_string(), self.choices, value))
1177 stdout_to = []
1178 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001179 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001180 elif value != 'none':
1181 stdout_to = [value]
1182 return stdout_to
1183
1184
maruel@chromium.org0437a732013-08-27 16:05:52 +00001185def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001186 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001187 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001188 help='Timeout to wait for result, set to -1 for no timeout and get '
1189 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001190 parser.group_logging.add_option(
1191 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001192 parser.group_logging.add_option(
1193 '--print-status-updates', action='store_true',
1194 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001195 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001196 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001197 '--task-summary-json',
1198 metavar='FILE',
1199 help='Dump a summary of task results to this file as json. It contains '
1200 'only shards statuses as know to server directly. Any output files '
1201 'emitted by the task can be collected by using --task-output-dir')
1202 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001203 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001204 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001205 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001206 'directory contains per-shard directory with output files produced '
1207 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001208 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001209 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001210 parser.task_output_group.add_option(
1211 '--perf', action='store_true', default=False,
1212 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001213 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001214
1215
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001216def process_collect_options(parser, options):
1217 # Only negative -1 is allowed, disallow other negative values.
1218 if options.timeout != -1 and options.timeout < 0:
1219 parser.error('Invalid --timeout value')
1220
1221
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001222@subcommand.usage('bots...')
1223def CMDbot_delete(parser, args):
1224 """Forcibly deletes bots from the Swarming server."""
1225 parser.add_option(
1226 '-f', '--force', action='store_true',
1227 help='Do not prompt for confirmation')
1228 options, args = parser.parse_args(args)
1229 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001230 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001231
1232 bots = sorted(args)
1233 if not options.force:
1234 print('Delete the following bots?')
1235 for bot in bots:
1236 print(' %s' % bot)
1237 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1238 print('Goodbye.')
1239 return 1
1240
1241 result = 0
1242 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001243 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001244 if net.url_read_json(url, data={}, method='POST') is None:
1245 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001246 result = 1
1247 return result
1248
1249
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001250def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001251 """Returns information about the bots connected to the Swarming server."""
1252 add_filter_options(parser)
1253 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001254 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001255 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001256 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001257 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001258 help='Keep both dead and alive bots')
1259 parser.filter_group.add_option(
1260 '--busy', action='store_true', help='Keep only busy bots')
1261 parser.filter_group.add_option(
1262 '--idle', action='store_true', help='Keep only idle bots')
1263 parser.filter_group.add_option(
1264 '--mp', action='store_true',
1265 help='Keep only Machine Provider managed bots')
1266 parser.filter_group.add_option(
1267 '--non-mp', action='store_true',
1268 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001269 parser.filter_group.add_option(
1270 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001271 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001272 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001273 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001274
1275 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001276 parser.error('Use only one of --keep-dead or --dead-only')
1277 if options.busy and options.idle:
1278 parser.error('Use only one of --busy or --idle')
1279 if options.mp and options.non_mp:
1280 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001281
smut281c3902018-05-30 17:50:05 -07001282 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001283 values = []
1284 if options.dead_only:
1285 values.append(('is_dead', 'TRUE'))
1286 elif options.keep_dead:
1287 values.append(('is_dead', 'NONE'))
1288 else:
1289 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001290
maruelaf6b06c2017-06-08 06:26:53 -07001291 if options.busy:
1292 values.append(('is_busy', 'TRUE'))
1293 elif options.idle:
1294 values.append(('is_busy', 'FALSE'))
1295 else:
1296 values.append(('is_busy', 'NONE'))
1297
1298 if options.mp:
1299 values.append(('is_mp', 'TRUE'))
1300 elif options.non_mp:
1301 values.append(('is_mp', 'FALSE'))
1302 else:
1303 values.append(('is_mp', 'NONE'))
1304
1305 for key, value in options.dimensions:
1306 values.append(('dimensions', '%s:%s' % (key, value)))
1307 url += urllib.urlencode(values)
1308 try:
1309 data, yielder = get_yielder(url, 0)
1310 bots = data.get('items') or []
1311 for items in yielder():
1312 if items:
1313 bots.extend(items)
1314 except Failure as e:
1315 sys.stderr.write('\n%s\n' % e)
1316 return 1
maruel77f720b2015-09-15 12:35:22 -07001317 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001318 print bot['bot_id']
1319 if not options.bare:
1320 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1321 print ' %s' % json.dumps(dimensions, sort_keys=True)
1322 if bot.get('task_id'):
1323 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001324 return 0
1325
1326
maruelfd0a90c2016-06-10 11:51:10 -07001327@subcommand.usage('task_id')
1328def CMDcancel(parser, args):
1329 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001330 parser.add_option(
1331 '-k', '--kill-running', action='store_true', default=False,
1332 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001333 options, args = parser.parse_args(args)
1334 if not args:
1335 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001336 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001337 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001338 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001339 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001340 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001341 print('Deleting %s failed. Probably already gone' % task_id)
1342 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001343 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001344 return 0
1345
1346
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001347@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001348def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001349 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001350
1351 The result can be in multiple part if the execution was sharded. It can
1352 potentially have retries.
1353 """
1354 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001355 parser.add_option(
1356 '-j', '--json',
1357 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001358 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001359 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001360 if not args and not options.json:
1361 parser.error('Must specify at least one task id or --json.')
1362 if args and options.json:
1363 parser.error('Only use one of task id or --json.')
1364
1365 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001366 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001367 try:
maruel1ceb3872015-10-14 06:10:44 -07001368 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001369 data = json.load(f)
1370 except (IOError, ValueError):
1371 parser.error('Failed to open %s' % options.json)
1372 try:
1373 tasks = sorted(
1374 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1375 args = [t['task_id'] for t in tasks]
1376 except (KeyError, TypeError):
1377 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001378 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001379 # Take in account all the task slices.
1380 offset = 0
1381 for s in data['request']['task_slices']:
1382 m = (offset + s['properties']['execution_timeout_secs'] +
1383 s['expiration_secs'])
1384 if m > options.timeout:
1385 options.timeout = m
1386 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001387 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001388 else:
1389 valid = frozenset('0123456789abcdef')
1390 if any(not valid.issuperset(task_id) for task_id in args):
1391 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001392
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001393 try:
1394 return collect(
1395 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001396 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001397 options.timeout,
1398 options.decorate,
1399 options.print_status_updates,
1400 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001401 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001402 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001403 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001404 except Failure:
1405 on_error.report(None)
1406 return 1
1407
1408
maruel77f720b2015-09-15 12:35:22 -07001409@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001410def CMDpost(parser, args):
1411 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1412
1413 Input data must be sent to stdin, result is printed to stdout.
1414
1415 If HTTP response code >= 400, returns non-zero.
1416 """
1417 options, args = parser.parse_args(args)
1418 if len(args) != 1:
1419 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001420 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001421 data = sys.stdin.read()
1422 try:
1423 resp = net.url_read(url, data=data, method='POST')
1424 except net.TimeoutError:
1425 sys.stderr.write('Timeout!\n')
1426 return 1
1427 if not resp:
1428 sys.stderr.write('No response!\n')
1429 return 1
1430 sys.stdout.write(resp)
1431 return 0
1432
1433
1434@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001435def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001436 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1437 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001438
1439 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001440 Raw task request and results:
1441 swarming.py query -S server-url.com task/123456/request
1442 swarming.py query -S server-url.com task/123456/result
1443
maruel77f720b2015-09-15 12:35:22 -07001444 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001445 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001446
maruelaf6b06c2017-06-08 06:26:53 -07001447 Listing last 10 tasks on a specific bot named 'bot1':
1448 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001449
maruelaf6b06c2017-06-08 06:26:53 -07001450 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001451 quoting is important!:
1452 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001453 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001454 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001455 parser.add_option(
1456 '-L', '--limit', type='int', default=200,
1457 help='Limit to enforce on limitless items (like number of tasks); '
1458 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001459 parser.add_option(
1460 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001461 parser.add_option(
1462 '--progress', action='store_true',
1463 help='Prints a dot at each request to show progress')
1464 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001465 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001466 parser.error(
1467 'Must specify only method name and optionally query args properly '
1468 'escaped.')
smut281c3902018-05-30 17:50:05 -07001469 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001470 try:
1471 data, yielder = get_yielder(base_url, options.limit)
1472 for items in yielder():
1473 if items:
1474 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001475 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001476 sys.stderr.write('.')
1477 sys.stderr.flush()
1478 except Failure as e:
1479 sys.stderr.write('\n%s\n' % e)
1480 return 1
maruel77f720b2015-09-15 12:35:22 -07001481 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001482 sys.stderr.write('\n')
1483 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001484 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001485 options.json = unicode(os.path.abspath(options.json))
1486 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001487 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001488 try:
maruel77f720b2015-09-15 12:35:22 -07001489 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001490 sys.stdout.write('\n')
1491 except IOError:
1492 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001493 return 0
1494
1495
maruel77f720b2015-09-15 12:35:22 -07001496def CMDquery_list(parser, args):
1497 """Returns list of all the Swarming APIs that can be used with command
1498 'query'.
1499 """
1500 parser.add_option(
1501 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1502 options, args = parser.parse_args(args)
1503 if args:
1504 parser.error('No argument allowed.')
1505
1506 try:
1507 apis = endpoints_api_discovery_apis(options.swarming)
1508 except APIError as e:
1509 parser.error(str(e))
1510 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001511 options.json = unicode(os.path.abspath(options.json))
1512 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001513 json.dump(apis, f)
1514 else:
1515 help_url = (
1516 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1517 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001518 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1519 if i:
1520 print('')
maruel77f720b2015-09-15 12:35:22 -07001521 print api_id
maruel11e31af2017-02-15 07:30:50 -08001522 print ' ' + api['description'].strip()
1523 if 'resources' in api:
1524 # Old.
1525 for j, (resource_name, resource) in enumerate(
1526 sorted(api['resources'].iteritems())):
1527 if j:
1528 print('')
1529 for method_name, method in sorted(resource['methods'].iteritems()):
1530 # Only list the GET ones.
1531 if method['httpMethod'] != 'GET':
1532 continue
1533 print '- %s.%s: %s' % (
1534 resource_name, method_name, method['path'])
1535 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001536 ' ' + l for l in textwrap.wrap(
1537 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001538 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1539 else:
1540 # New.
1541 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001542 # Only list the GET ones.
1543 if method['httpMethod'] != 'GET':
1544 continue
maruel11e31af2017-02-15 07:30:50 -08001545 print '- %s: %s' % (method['id'], method['path'])
1546 print('\n'.join(
1547 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001548 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1549 return 0
1550
1551
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001552@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001553def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001554 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001555
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001556 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001557 """
1558 add_trigger_options(parser)
1559 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001560 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001561 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001562 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001563 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001564 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001565 tasks = trigger_task_shards(
1566 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001567 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001568 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001569 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001570 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001571 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001572 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001573 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001574 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001575 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001576 task_ids = [
1577 t['task_id']
1578 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1579 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001580 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001581 offset = 0
1582 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001583 m = (offset + s.properties.execution_timeout_secs +
1584 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001585 if m > options.timeout:
1586 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001587 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001588 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001589 try:
1590 return collect(
1591 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001592 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001593 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001594 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001595 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001596 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001597 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001598 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001599 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001600 except Failure:
1601 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001602 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001603
1604
maruel18122c62015-10-23 06:31:23 -07001605@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001606def CMDreproduce(parser, args):
1607 """Runs a task locally that was triggered on the server.
1608
1609 This running locally the same commands that have been run on the bot. The data
1610 downloaded will be in a subdirectory named 'work' of the current working
1611 directory.
maruel18122c62015-10-23 06:31:23 -07001612
1613 You can pass further additional arguments to the target command by passing
1614 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001615 """
maruelc070e672016-02-22 17:32:57 -08001616 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001617 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001618 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001619 parser.add_option(
1620 '--work', metavar='DIR', default='work',
1621 help='Directory to map the task input files into')
1622 parser.add_option(
1623 '--cache', metavar='DIR', default='cache',
1624 help='Directory that contains the input cache')
1625 parser.add_option(
1626 '--leak', action='store_true',
1627 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001628 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001629 extra_args = []
1630 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001631 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001632 if len(args) > 1:
1633 if args[1] == '--':
1634 if len(args) > 2:
1635 extra_args = args[2:]
1636 else:
1637 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001638
smut281c3902018-05-30 17:50:05 -07001639 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001640 request = net.url_read_json(url)
1641 if not request:
1642 print >> sys.stderr, 'Failed to retrieve request data for the task'
1643 return 1
1644
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001645 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001646 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001647 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001648 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001649 cachedir = unicode(os.path.abspath('cipd_cache'))
1650 if not fs.exists(cachedir):
1651 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001652
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001653 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001654 env = os.environ.copy()
1655 env['SWARMING_BOT_ID'] = 'reproduce'
1656 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001657 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001658 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001659 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001660 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001661 if not i['value']:
1662 env.pop(key, None)
1663 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001664 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001665
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001666 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001667 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001668 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001669 for i in env_prefixes:
1670 key = i['key']
1671 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001672 cur = env.get(key)
1673 if cur:
1674 paths.append(cur)
1675 env[key] = os.path.pathsep.join(paths)
1676
iannucci31ab9192017-05-02 19:11:56 -07001677 command = []
nodir152cba62016-05-12 16:08:56 -07001678 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001679 # Create the tree.
1680 with isolateserver.get_storage(
1681 properties['inputs_ref']['isolatedserver'],
1682 properties['inputs_ref']['namespace']) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001683 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1684 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1685 # leak.
1686 policies = local_caching.CachePolicies(0, 0, 0, 0)
1687 algo = isolated_format.get_hash_algo(
1688 properties['inputs_ref']['namespace'])
1689 cache = local_caching.DiskContentAddressedCache(
1690 unicode(os.path.abspath(options.cache)), policies, algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001691 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001692 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001693 command = bundle.command
1694 if bundle.relative_cwd:
1695 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001696 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001697
1698 if properties.get('command'):
1699 command.extend(properties['command'])
1700
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001701 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001702 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001703 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001704 new_command = run_isolated.process_command(command, 'invalid', None)
1705 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001706 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001707 else:
1708 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001709 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001710 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001711 command, options.output, None)
1712 if not os.path.isdir(options.output):
1713 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001714 command = new_command
1715 file_path.ensure_command_has_abs_path(command, workdir)
1716
1717 if properties.get('cipd_input'):
1718 ci = properties['cipd_input']
1719 cp = ci['client_package']
1720 client_manager = cipd.get_client(
1721 ci['server'], cp['package_name'], cp['version'], cachedir)
1722
1723 with client_manager as client:
1724 by_path = collections.defaultdict(list)
1725 for pkg in ci['packages']:
1726 path = pkg['path']
1727 # cipd deals with 'root' as ''
1728 if path == '.':
1729 path = ''
1730 by_path[path].append((pkg['package_name'], pkg['version']))
1731 client.ensure(workdir, by_path, cache_dir=cachedir)
1732
maruel77f720b2015-09-15 12:35:22 -07001733 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001734 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001735 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001736 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001737 print >> sys.stderr, str(e)
1738 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001739 finally:
1740 # Do not delete options.cache.
1741 if not options.leak:
1742 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001743
1744
maruel0eb1d1b2015-10-02 14:48:21 -07001745@subcommand.usage('bot_id')
1746def CMDterminate(parser, args):
1747 """Tells a bot to gracefully shut itself down as soon as it can.
1748
1749 This is done by completing whatever current task there is then exiting the bot
1750 process.
1751 """
1752 parser.add_option(
1753 '--wait', action='store_true', help='Wait for the bot to terminate')
1754 options, args = parser.parse_args(args)
1755 if len(args) != 1:
1756 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001757 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001758 request = net.url_read_json(url, data={})
1759 if not request:
1760 print >> sys.stderr, 'Failed to ask for termination'
1761 return 1
1762 if options.wait:
1763 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001764 options.swarming,
1765 [request['task_id']],
1766 0.,
1767 False,
1768 False,
1769 None,
1770 None,
1771 [],
maruel9531ce02016-04-13 06:11:23 -07001772 False)
maruelbfc5f872017-06-10 16:43:17 -07001773 else:
1774 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001775 return 0
1776
1777
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001778@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001779def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001780 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001781
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001782 Passes all extra arguments provided after '--' as additional command line
1783 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001784 """
1785 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001786 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001787 parser.add_option(
1788 '--dump-json',
1789 metavar='FILE',
1790 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001791 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001792 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001793 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001794 tasks = trigger_task_shards(
1795 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001796 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001797 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001798 tasks_sorted = sorted(
1799 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001800 if options.dump_json:
1801 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001802 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001803 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001804 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001805 }
maruel46b015f2015-10-13 18:40:35 -07001806 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001807 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001808 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001809 (options.swarming, options.dump_json))
1810 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001811 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001812 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001813 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1814 print('Or visit:')
1815 for t in tasks_sorted:
1816 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001817 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001818 except Failure:
1819 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001820 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001821
1822
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001823class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001824 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001825 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001826 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001827 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001828 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001829 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001830 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001831 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001832 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001833 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001834
1835 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001836 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001837 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001838 auth.process_auth_options(self, options)
1839 user = self._process_swarming(options)
1840 if hasattr(options, 'user') and not options.user:
1841 options.user = user
1842 return options, args
1843
1844 def _process_swarming(self, options):
1845 """Processes the --swarming option and aborts if not specified.
1846
1847 Returns the identity as determined by the server.
1848 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001849 if not options.swarming:
1850 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001851 try:
1852 options.swarming = net.fix_url(options.swarming)
1853 except ValueError as e:
1854 self.error('--swarming %s' % e)
1855 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001856 try:
1857 user = auth.ensure_logged_in(options.swarming)
1858 except ValueError as e:
1859 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001860 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001861
1862
1863def main(args):
1864 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001865 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001866
1867
1868if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001869 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001870 fix_encoding.fix_encoding()
1871 tools.disable_buffering()
1872 colorama.init()
1873 sys.exit(main(sys.argv[1:]))