blob: def1794d842028b60fa61b6d17b631b6cb0539cd [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"""Client tool to trigger tasks or retrieve results from a Swarming server."""
6
Lei Leife202df2019-06-11 17:33:34 +00007from __future__ import print_function
8
9__version__ = '1.0'
maruel@chromium.org0437a732013-08-27 16:05:52 +000010
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050011import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040012import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000013import json
14import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040015import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import os
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100017import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000018import sys
maruel11e31af2017-02-15 07:30:50 -080019import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070020import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000021import time
Takuto Ikuta35250172020-01-31 09:33:46 +000022import uuid
maruel@chromium.org0437a732013-08-27 16:05:52 +000023
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000024from utils import tools
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000025tools.force_local_third_party()
maruel@chromium.org0437a732013-08-27 16:05:52 +000026
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000027# third_party/
28import colorama
29from chromium import natsort
30from depot_tools import fix_encoding
31from depot_tools import subcommand
Takuto Ikuta6e2ff962019-10-29 12:35:27 +000032import six
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +000033from six.moves import urllib
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000034
35# pylint: disable=ungrouped-imports
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036import auth
iannucci31ab9192017-05-02 19:11:56 -070037import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000038import isolateserver
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +000039import isolate_storage
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040040import local_caching
maruelc070e672016-02-22 17:32:57 -080041import run_isolated
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000042from utils import file_path
43from utils import fs
44from utils import logging_utils
45from utils import net
46from utils import on_error
47from utils import subprocess42
48from utils import threading_utils
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050049
50
51class Failure(Exception):
52 """Generic failure."""
53 pass
54
55
maruel0a25f6c2017-05-10 10:43:23 -070056def default_task_name(options):
57 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050058 if not options.task_name:
Junji Watanabe38b28b02020-04-23 10:23:30 +000059 task_name = u'%s/%s' % (options.user, '_'.join(
60 '%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070061 if options.isolated:
62 task_name += u'/' + options.isolated
63 return task_name
64 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050065
66
67### Triggering.
68
maruel77f720b2015-09-15 12:35:22 -070069# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000070CipdPackage = collections.namedtuple('CipdPackage', [
71 'package_name',
72 'path',
73 'version',
74])
borenet02f772b2016-06-22 12:42:19 -070075
borenet02f772b2016-06-22 12:42:19 -070076# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000077CipdInput = collections.namedtuple('CipdInput', [
78 'client_package',
79 'packages',
80 'server',
81])
borenet02f772b2016-06-22 12:42:19 -070082
83# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000084FilesRef = collections.namedtuple('FilesRef', [
85 'isolated',
86 'isolatedserver',
87 'namespace',
88])
maruel77f720b2015-09-15 12:35:22 -070089
90# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080091StringListPair = collections.namedtuple(
Junji Watanabe38b28b02020-04-23 10:23:30 +000092 'StringListPair',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000093 [
Junji Watanabe38b28b02020-04-23 10:23:30 +000094 'key',
95 'value', # repeated string
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000096 ])
97
Junji Watanabe38b28b02020-04-23 10:23:30 +000098# See ../appengine/swarming/swarming_rpcs.py.
99Containment = collections.namedtuple('Containment', [
100 'lower_priority',
101 'containment_type',
102])
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800103
104# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +0000105TaskProperties = collections.namedtuple('TaskProperties', [
106 'caches',
107 'cipd_input',
108 'command',
109 'containment',
110 'relative_cwd',
111 'dimensions',
112 'env',
113 'env_prefixes',
114 'execution_timeout_secs',
115 'extra_args',
116 'grace_period_secs',
117 'idempotent',
118 'inputs_ref',
119 'io_timeout_secs',
120 'outputs',
121 'secret_bytes',
122])
maruel77f720b2015-09-15 12:35:22 -0700123
Junji Watanabecb054042020-07-21 08:43:26 +0000124# See ../appengine/swarming/swarming_rpcs.py.
125TaskSlice = collections.namedtuple('TaskSlice', [
126 'expiration_secs',
127 'properties',
128 'wait_for_capacity',
129])
maruel77f720b2015-09-15 12:35:22 -0700130
131# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabecb054042020-07-21 08:43:26 +0000132NewTaskRequest = collections.namedtuple('NewTaskRequest', [
133 'name',
134 'parent_task_id',
135 'priority',
Junji Watanabe71bbaef2020-07-21 08:55:37 +0000136 'realm',
Scott Lee44c13d72020-09-14 06:09:50 +0000137 'resultdb',
Junji Watanabecb054042020-07-21 08:43:26 +0000138 'task_slices',
139 'service_account',
140 'tags',
141 'user',
142 'pool_task_template',
143])
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144
145
maruel77f720b2015-09-15 12:35:22 -0700146def namedtuple_to_dict(value):
147 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400148 if hasattr(value, '_asdict'):
149 return namedtuple_to_dict(value._asdict())
150 if isinstance(value, (list, tuple)):
151 return [namedtuple_to_dict(v) for v in value]
152 if isinstance(value, dict):
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000153 return {k: namedtuple_to_dict(v) for k, v in value.items()}
Lei Lei73a5f732020-03-23 20:36:14 +0000154 # json.dumps in Python3 doesn't support bytes.
155 if isinstance(value, bytes):
156 return six.ensure_str(value)
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400157 return value
maruel77f720b2015-09-15 12:35:22 -0700158
159
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700160def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800161 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700162
163 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 """
maruel77f720b2015-09-15 12:35:22 -0700165 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700166 # Don't send 'service_account' if it is None to avoid confusing older
167 # version of the server that doesn't know about 'service_account' and don't
168 # use it at all.
169 if not out['service_account']:
170 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000171 for task_slice in out['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +0000172 task_slice['properties']['env'] = [{
173 'key': k,
174 'value': v
175 } for k, v in task_slice['properties']['env'].items()]
Brad Hallf78187a2018-10-19 17:08:55 +0000176 task_slice['properties']['env'].sort(key=lambda x: x['key'])
Takuto Ikuta35250172020-01-31 09:33:46 +0000177 out['request_uuid'] = str(uuid.uuid4())
maruel77f720b2015-09-15 12:35:22 -0700178 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500179
180
maruel77f720b2015-09-15 12:35:22 -0700181def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500182 """Triggers a request on the Swarming server and returns the json data.
183
184 It's the low-level function.
185
186 Returns:
187 {
188 'request': {
189 'created_ts': u'2010-01-02 03:04:05',
190 'name': ..
191 },
192 'task_id': '12300',
193 }
194 """
195 logging.info('Triggering: %s', raw_request['name'])
196
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700198 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199 if not result:
200 on_error.report('Failed to trigger task %s' % raw_request['name'])
201 return None
maruele557bce2015-11-17 09:01:27 -0800202 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800203 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800204 msg = 'Failed to trigger task %s' % raw_request['name']
205 if result['error'].get('errors'):
206 for err in result['error']['errors']:
207 if err.get('message'):
208 msg += '\nMessage: %s' % err['message']
209 if err.get('debugInfo'):
210 msg += '\nDebug info:\n%s' % err['debugInfo']
211 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800212 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800213
214 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800215 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500216 return result
217
218
219def setup_googletest(env, shards, index):
220 """Sets googletest specific environment variables."""
221 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700222 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
223 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
224 env = env[:]
225 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
226 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500227 return env
228
229
230def trigger_task_shards(swarming, task_request, shards):
231 """Triggers one or many subtasks of a sharded task.
232
233 Returns:
234 Dict with task details, returned to caller as part of --dump-json output.
235 None in case of failure.
236 """
Junji Watanabecb054042020-07-21 08:43:26 +0000237
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500238 def convert(index):
Erik Chend50a88f2019-02-16 01:22:07 +0000239 """
240 Args:
241 index: The index of the task request.
242
243 Returns:
244 raw_request: A swarming compatible JSON dictionary of the request.
245 shard_index: The index of the shard, which may be different than the index
246 of the task request.
247 """
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700248 req = task_request_to_raw_request(task_request)
Erik Chend50a88f2019-02-16 01:22:07 +0000249 shard_index = index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250 if shards > 1:
Brad Hall157bec82018-11-26 22:15:38 +0000251 for task_slice in req['task_slices']:
252 task_slice['properties']['env'] = setup_googletest(
253 task_slice['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700254 req['name'] += ':%s:%s' % (index, shards)
Erik Chend50a88f2019-02-16 01:22:07 +0000255 else:
256 task_slices = req['task_slices']
257
Lei Lei73a5f732020-03-23 20:36:14 +0000258 total_shards = 1
Erik Chend50a88f2019-02-16 01:22:07 +0000259 # Multiple tasks slices might exist if there are optional "slices", e.g.
260 # multiple ways of dispatching the task that should be equivalent. These
261 # should be functionally equivalent but we have cannot guarantee that. If
262 # we see the GTEST_SHARD_INDEX env var, we assume that it applies to all
263 # slices.
264 for task_slice in task_slices:
265 for env_var in task_slice['properties']['env']:
266 if env_var['key'] == 'GTEST_SHARD_INDEX':
267 shard_index = int(env_var['value'])
268 if env_var['key'] == 'GTEST_TOTAL_SHARDS':
269 total_shards = int(env_var['value'])
270 if total_shards > 1:
271 req['name'] += ':%s:%s' % (shard_index, total_shards)
Ben Pastened2a7be42020-07-14 22:28:55 +0000272 if shard_index and total_shards:
273 req['tags'] += [
274 'shard_index:%d' % shard_index,
275 'total_shards:%d' % total_shards,
276 ]
Erik Chend50a88f2019-02-16 01:22:07 +0000277
278 return req, shard_index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500279
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000280 requests = [convert(index) for index in range(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500281 tasks = {}
282 priority_warning = False
Erik Chend50a88f2019-02-16 01:22:07 +0000283 for request, shard_index in requests:
maruel77f720b2015-09-15 12:35:22 -0700284 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500285 if not task:
286 break
287 logging.info('Request result: %s', task)
288 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400289 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500290 priority_warning = True
Junji Watanabecb054042020-07-21 08:43:26 +0000291 print(
292 'Priority was reset to %s' % task['request']['priority'],
293 file=sys.stderr)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500294 tasks[request['name']] = {
Junji Watanabecb054042020-07-21 08:43:26 +0000295 'shard_index': shard_index,
296 'task_id': task['task_id'],
297 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500298 }
299
300 # Some shards weren't triggered. Abort everything.
301 if len(tasks) != len(requests):
302 if tasks:
Junji Watanabecb054042020-07-21 08:43:26 +0000303 print(
304 'Only %d shard(s) out of %d were triggered' %
305 (len(tasks), len(requests)),
306 file=sys.stderr)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000307 for task_dict in tasks.values():
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500308 abort_task(swarming, task_dict['task_id'])
309 return None
310
311 return tasks
312
313
314### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000315
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700316# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000317STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700318
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400319
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000320class TaskState(object):
321 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000322
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000323 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
324 is the source of truth for these values:
325 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400326
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000327 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400328 """
329 RUNNING = 0x10
330 PENDING = 0x20
331 EXPIRED = 0x30
332 TIMED_OUT = 0x40
333 BOT_DIED = 0x50
334 CANCELED = 0x60
335 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400336 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400337 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400338
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000339 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400340
maruel77f720b2015-09-15 12:35:22 -0700341 _ENUMS = {
Junji Watanabecb054042020-07-21 08:43:26 +0000342 'RUNNING': RUNNING,
343 'PENDING': PENDING,
344 'EXPIRED': EXPIRED,
345 'TIMED_OUT': TIMED_OUT,
346 'BOT_DIED': BOT_DIED,
347 'CANCELED': CANCELED,
348 'COMPLETED': COMPLETED,
349 'KILLED': KILLED,
350 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700351 }
352
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400353 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700354 def from_enum(cls, state):
355 """Returns int value based on the string."""
356 if state not in cls._ENUMS:
357 raise ValueError('Invalid state %s' % state)
358 return cls._ENUMS[state]
359
maruel@chromium.org0437a732013-08-27 16:05:52 +0000360
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700362 """Assembles task execution summary (for --task-summary-json output).
363
364 Optionally fetches task outputs from isolate server to local disk (used when
365 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700366
367 This object is shared among multiple threads running 'retrieve_results'
368 function, in particular they call 'process_shard_result' method in parallel.
369 """
370
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000371 def __init__(self, task_output_dir, task_output_stdout, shard_count,
372 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700373 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
374
375 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700376 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377 shard_count: expected number of task shards.
378 """
maruel12e30012015-10-09 11:55:35 -0700379 self.task_output_dir = (
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000380 six.text_type(os.path.abspath(task_output_dir))
maruel12e30012015-10-09 11:55:35 -0700381 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000382 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700383 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000384 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385
386 self._lock = threading.Lock()
387 self._per_shard_results = {}
388 self._storage = None
389
nodire5028a92016-04-29 14:38:21 -0700390 if self.task_output_dir:
391 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700392
Vadim Shtayurab450c602014-05-12 19:23:25 -0700393 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700394 """Stores results of a single task shard, fetches output files if necessary.
395
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400396 Modifies |result| in place.
397
maruel77f720b2015-09-15 12:35:22 -0700398 shard_index is 0-based.
399
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400 Called concurrently from multiple threads.
401 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700403 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700404 if shard_index < 0 or shard_index >= self.shard_count:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000405 logging.warning('Shard index %d is outside of expected range: [0; %d]',
406 shard_index, self.shard_count - 1)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700407 return
408
maruel77f720b2015-09-15 12:35:22 -0700409 if result.get('outputs_ref'):
410 ref = result['outputs_ref']
411 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
412 ref['isolatedserver'],
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000413 urllib.parse.urlencode([('namespace', ref['namespace']),
414 ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400415
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700416 # Store result dict of that shard, ignore results we've already seen.
417 with self._lock:
418 if shard_index in self._per_shard_results:
419 logging.warning('Ignoring duplicate shard index %d', shard_index)
420 return
421 self._per_shard_results[shard_index] = result
422
423 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700424 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000425 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +0000426 result['outputs_ref']['isolatedserver'],
427 result['outputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000428 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400429 if storage:
430 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400431 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
432 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400433 isolateserver.fetch_isolated(
Junji Watanabecb054042020-07-21 08:43:26 +0000434 result['outputs_ref']['isolated'], storage,
Lei Leife202df2019-06-11 17:33:34 +0000435 local_caching.MemoryContentAddressedCache(file_mode_mask=0o700),
Junji Watanabecb054042020-07-21 08:43:26 +0000436 os.path.join(self.task_output_dir, str(shard_index)), False,
437 self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438
439 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441 with self._lock:
442 # Write an array of shard results with None for missing shards.
443 summary = {
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000444 'shards': [
445 self._per_shard_results.get(i) for i in range(self.shard_count)
446 ],
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000448
449 # Don't store stdout in the summary if not requested too.
450 if "json" not in self.task_output_stdout:
451 for shard_json in summary['shards']:
452 if not shard_json:
453 continue
454 if "output" in shard_json:
455 del shard_json["output"]
456 if "outputs" in shard_json:
457 del shard_json["outputs"]
458
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700459 # Write summary.json to task_output_dir as well.
460 if self.task_output_dir:
461 tools.write_json(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000462 os.path.join(self.task_output_dir, u'summary.json'), summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700463 if self._storage:
464 self._storage.close()
465 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700466 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700467
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000468 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700469 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700470 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700471 with self._lock:
472 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000473 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700474 else:
475 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000476 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700477 logging.error(
478 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000479 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700480 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000481 if self._storage.server_ref.namespace != server_ref.namespace:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000482 logging.error('Task shards are using multiple namespaces: %s and %s',
483 self._storage.server_ref.namespace,
484 server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700485 return None
486 return self._storage
487
488
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500489def now():
490 """Exists so it can be mocked easily."""
491 return time.time()
492
493
maruel77f720b2015-09-15 12:35:22 -0700494def parse_time(value):
495 """Converts serialized time from the API to datetime.datetime."""
496 # When microseconds are 0, the '.123456' suffix is elided. This means the
497 # serialized format is not consistent, which confuses the hell out of python.
498 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
499 try:
500 return datetime.datetime.strptime(value, fmt)
501 except ValueError:
502 pass
503 raise ValueError('Failed to parse %s' % value)
504
505
Junji Watanabe38b28b02020-04-23 10:23:30 +0000506def retrieve_results(base_url, shard_index, task_id, timeout, should_stop,
507 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400508 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700509
Vadim Shtayurab450c602014-05-12 19:23:25 -0700510 Returns:
511 <result dict> on success.
512 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700513 """
maruel71c61c82016-02-22 06:52:05 -0800514 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700515 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700516 if include_perf:
517 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700518 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700519 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400520 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700521 attempt = 0
522
523 while not should_stop.is_set():
524 attempt += 1
525
526 # Waiting for too long -> give up.
527 current_time = now()
528 if deadline and current_time >= deadline:
Junji Watanabecb054042020-07-21 08:43:26 +0000529 logging.error('retrieve_results(%s) timed out on attempt %d', base_url,
530 attempt)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700531 return None
532
533 # Do not spin too fast. Spin faster at the beginning though.
534 # Start with 1 sec delay and for each 30 sec of waiting add another second
535 # of delay, until hitting 15 sec ceiling.
536 if attempt > 1:
537 max_delay = min(15, 1 + (current_time - started) / 30.0)
538 delay = min(max_delay, deadline - current_time) if deadline else max_delay
539 if delay > 0:
540 logging.debug('Waiting %.1f sec before retrying', delay)
541 should_stop.wait(delay)
542 if should_stop.is_set():
543 return None
544
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400545 # Disable internal retries in net.url_read_json, since we are doing retries
546 # ourselves.
547 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700548 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
549 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400550 # Retry on 500s only if no timeout is specified.
551 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400552 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400553 if timeout == -1:
554 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400555 continue
maruel77f720b2015-09-15 12:35:22 -0700556
maruelbf53e042015-12-01 15:00:51 -0800557 if result.get('error'):
558 # An error occurred.
559 if result['error'].get('errors'):
560 for err in result['error']['errors']:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000561 logging.warning('Error while reading task: %s; %s',
562 err.get('message'), err.get('debugInfo'))
maruelbf53e042015-12-01 15:00:51 -0800563 elif result['error'].get('message'):
Junji Watanabecb054042020-07-21 08:43:26 +0000564 logging.warning('Error while reading task: %s',
565 result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400566 if timeout == -1:
567 return result
maruelbf53e042015-12-01 15:00:51 -0800568 continue
569
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400570 # When timeout == -1, always return on first attempt. 500s are already
571 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000572 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000573 if fetch_stdout:
574 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700575 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700576 # Record the result, try to fetch attached output files (if any).
577 if output_collector:
578 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700579 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700580 if result.get('internal_failure'):
581 logging.error('Internal error!')
582 elif result['state'] == 'BOT_DIED':
583 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700584 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000585
586
Junji Watanabecb054042020-07-21 08:43:26 +0000587def yield_results(swarm_base_url, task_ids, timeout, max_threads,
588 print_status_updates, output_collector, include_perf,
589 fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500590 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000591
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700592 Duplicate shards are ignored. Shards are yielded in order of completion.
593 Timed out shards are NOT yielded at all. Caller can compare number of yielded
594 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000595
596 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500597 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 +0000598 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500599
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700600 output_collector is an optional instance of TaskOutputCollector that will be
601 used to fetch files produced by a task from isolate server to the local disk.
602
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500603 Yields:
604 (index, result). In particular, 'result' is defined as the
605 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000606 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000607 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400608 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700609 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700610 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700611
maruel@chromium.org0437a732013-08-27 16:05:52 +0000612 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
613 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700614 # Adds a task to the thread pool to call 'retrieve_results' and return
615 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400616 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000617 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700618 task_fn = lambda *args: (shard_index, retrieve_results(*args))
Junji Watanabecb054042020-07-21 08:43:26 +0000619 pool.add_task(0, results_channel.wrap_task(task_fn), swarm_base_url,
620 shard_index, task_id, timeout, should_stop,
621 output_collector, include_perf, fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700622
623 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400624 for shard_index, task_id in enumerate(task_ids):
625 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700626
627 # Wait for all of them to finish.
Lei Lei73a5f732020-03-23 20:36:14 +0000628 # Convert to list, since range in Python3 doesn't have remove.
629 shards_remaining = list(range(len(task_ids)))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400630 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700631 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700632 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700633 try:
Marc-Antoine Ruel4494b6c2018-11-28 21:00:41 +0000634 shard_index, result = results_channel.next(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700635 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700636 except threading_utils.TaskChannel.Timeout:
637 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000638 time_now = str(datetime.datetime.now())
639 _, time_now = time_now.split(' ')
Junji Watanabe38b28b02020-04-23 10:23:30 +0000640 print('%s '
641 'Waiting for results from the following shards: %s' %
642 (time_now, ', '.join(map(str, shards_remaining))))
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700643 sys.stdout.flush()
644 continue
645 except Exception:
646 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700647
648 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500651 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000652 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654 # Yield back results to the caller.
655 assert shard_index in shards_remaining
656 shards_remaining.remove(shard_index)
657 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700658
maruel@chromium.org0437a732013-08-27 16:05:52 +0000659 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000661 should_stop.set()
662
663
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000664def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000665 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700666 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Junji Watanabecb054042020-07-21 08:43:26 +0000667 pending = '%.1fs' % (parse_time(metadata['started_ts']) -
668 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400669 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
670 metadata.get('abandoned_ts')):
Junji Watanabecb054042020-07-21 08:43:26 +0000671 pending = '%.1fs' % (parse_time(metadata['abandoned_ts']) -
672 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400673 else:
674 pending = 'N/A'
675
maruel77f720b2015-09-15 12:35:22 -0700676 if metadata.get('duration') is not None:
677 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400678 else:
679 duration = 'N/A'
680
maruel77f720b2015-09-15 12:35:22 -0700681 if metadata.get('exit_code') is not None:
682 # Integers are encoded as string to not loose precision.
683 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400684 else:
685 exit_code = 'N/A'
686
687 bot_id = metadata.get('bot_id') or 'N/A'
688
maruel77f720b2015-09-15 12:35:22 -0700689 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400690 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000691 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400692 if metadata.get('state') == 'CANCELED':
693 tag_footer2 = ' Pending: %s CANCELED' % pending
694 elif metadata.get('state') == 'EXPIRED':
695 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400696 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400697 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
698 pending, duration, bot_id, exit_code, metadata['state'])
699 else:
700 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
701 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400702
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000703 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
704 dash_pad = '+-%s-+' % ('-' * tag_len)
705 tag_header = '| %s |' % tag_header.ljust(tag_len)
706 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
707 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400708
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000709 if include_stdout:
710 return '\n'.join([
711 dash_pad,
712 tag_header,
713 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400714 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000715 dash_pad,
716 tag_footer1,
717 tag_footer2,
718 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000719 ])
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000720 return '\n'.join([
721 dash_pad,
722 tag_header,
723 tag_footer2,
724 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000725 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000726
727
Junji Watanabecb054042020-07-21 08:43:26 +0000728def collect(swarming, task_ids, timeout, decorate, print_status_updates,
729 task_summary_json, task_output_dir, task_output_stdout,
730 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700731 """Retrieves results of a Swarming task.
732
733 Returns:
734 process exit code that should be returned to the user.
735 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000736
737 filter_cb = None
738 if filepath_filter:
739 filter_cb = re.compile(filepath_filter).match
740
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700741 # Collect summary JSON and output files (if task_output_dir is not None).
Junji Watanabecb054042020-07-21 08:43:26 +0000742 output_collector = TaskOutputCollector(task_output_dir, task_output_stdout,
743 len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700744
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700745 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700746 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400747 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700748 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400749 for index, metadata in yield_results(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000750 swarming,
751 task_ids,
752 timeout,
753 None,
754 print_status_updates,
755 output_collector,
756 include_perf,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000757 (len(task_output_stdout) > 0),
Junji Watanabe38b28b02020-04-23 10:23:30 +0000758 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700759 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700760
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400761 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700762 shard_exit_code = metadata.get('exit_code')
763 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700764 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700765 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700766 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400767 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700768 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700769
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700770 if decorate:
Lei Lei73a5f732020-03-23 20:36:14 +0000771 # s is bytes in Python3, print could not print
772 # s with nice format, so decode s to str.
773 s = six.ensure_str(
774 decorate_shard_output(swarming, index, metadata,
775 "console" in task_output_stdout).encode(
776 'utf-8', 'replace'))
leileied181762016-10-13 14:24:59 -0700777 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400778 if len(seen_shards) < len(task_ids):
779 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700780 else:
Junji Watanabecb054042020-07-21 08:43:26 +0000781 print('%s: %s %s' % (metadata.get(
782 'bot_id', 'N/A'), metadata['task_id'], shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000783 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700784 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400785 if output:
786 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700787 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700788 summary = output_collector.finalize()
789 if task_summary_json:
790 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700791
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400792 if decorate and total_duration:
793 print('Total duration: %.1fs' % total_duration)
794
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400795 if len(seen_shards) != len(task_ids):
796 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Junji Watanabecb054042020-07-21 08:43:26 +0000797 print(
798 'Results from some shards are missing: %s' %
799 ', '.join(map(str, missing_shards)),
800 file=sys.stderr)
Vadim Shtayurac524f512014-05-15 09:54:56 -0700801 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700802
maruela5490782015-09-30 10:56:59 -0700803 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000804
805
maruel77f720b2015-09-15 12:35:22 -0700806### API management.
807
808
809class APIError(Exception):
810 pass
811
812
813def endpoints_api_discovery_apis(host):
814 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
815 the APIs exposed by a host.
816
817 https://developers.google.com/discovery/v1/reference/apis/list
818 """
maruel380e3262016-08-31 16:10:06 -0700819 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
820 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700821 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
822 if data is None:
823 raise APIError('Failed to discover APIs on %s' % host)
824 out = {}
825 for api in data['items']:
826 if api['id'] == 'discovery:v1':
827 continue
828 # URL is of the following form:
829 # url = host + (
830 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
831 api_data = net.url_read_json(api['discoveryRestUrl'])
832 if api_data is None:
833 raise APIError('Failed to discover %s on %s' % (api['id'], host))
834 out[api['id']] = api_data
835 return out
836
837
maruelaf6b06c2017-06-08 06:26:53 -0700838def get_yielder(base_url, limit):
839 """Returns the first query and a function that yields following items."""
840 CHUNK_SIZE = 250
841
842 url = base_url
843 if limit:
844 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
845 data = net.url_read_json(url)
846 if data is None:
847 # TODO(maruel): Do basic diagnostic.
848 raise Failure('Failed to access %s' % url)
849 org_cursor = data.pop('cursor', None)
850 org_total = len(data.get('items') or [])
851 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
852 if not org_cursor or not org_total:
853 # This is not an iterable resource.
854 return data, lambda: []
855
856 def yielder():
857 cursor = org_cursor
858 total = org_total
859 # Some items support cursors. Try to get automatically if cursors are needed
860 # by looking at the 'cursor' items.
861 while cursor and (not limit or total < limit):
862 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000863 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -0700864 if limit:
865 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
866 new = net.url_read_json(url)
867 if new is None:
868 raise Failure('Failed to access %s' % url)
869 cursor = new.get('cursor')
870 new_items = new.get('items')
871 nb_items = len(new_items or [])
872 total += nb_items
873 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
874 yield new_items
875
876 return data, yielder
877
878
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500879### Commands.
880
881
882def abort_task(_swarming, _manifest):
883 """Given a task manifest that was triggered, aborts its execution."""
884 # TODO(vadimsh): No supported by the server yet.
885
886
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400887def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800888 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500889 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000890 '-d',
891 '--dimension',
892 default=[],
893 action='append',
894 nargs=2,
895 dest='dimensions',
896 metavar='FOO bar',
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500897 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000898 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000899 '--optional-dimension',
900 default=[],
901 action='append',
902 nargs=3,
903 dest='optional_dimensions',
904 metavar='key value expiration',
Brad Hallf78187a2018-10-19 17:08:55 +0000905 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500906 parser.add_option_group(parser.filter_group)
907
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400908
Brad Hallf78187a2018-10-19 17:08:55 +0000909def _validate_filter_option(parser, key, value, expiration, argname):
910 if ':' in key:
911 parser.error('%s key cannot contain ":"' % argname)
912 if key.strip() != key:
913 parser.error('%s key has whitespace' % argname)
914 if not key:
915 parser.error('%s key is empty' % argname)
916
917 if value.strip() != value:
918 parser.error('%s value has whitespace' % argname)
919 if not value:
920 parser.error('%s value is empty' % argname)
921
922 if expiration is not None:
923 try:
924 expiration = int(expiration)
925 except ValueError:
926 parser.error('%s expiration is not an integer' % argname)
927 if expiration <= 0:
928 parser.error('%s expiration should be positive' % argname)
929 if expiration % 60 != 0:
930 parser.error('%s expiration is not divisible by 60' % argname)
931
932
maruelaf6b06c2017-06-08 06:26:53 -0700933def process_filter_options(parser, options):
934 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000935 _validate_filter_option(parser, key, value, None, 'dimension')
936 for key, value, exp in options.optional_dimensions:
937 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700938 options.dimensions.sort()
939
940
Vadim Shtayurab450c602014-05-12 19:23:25 -0700941def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400942 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700943 parser.sharding_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000944 '--shards',
945 type='int',
946 default=1,
947 metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700948 help='Number of shards to trigger and collect.')
949 parser.add_option_group(parser.sharding_group)
950
951
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400952def add_trigger_options(parser):
953 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500954 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400955 add_filter_options(parser)
956
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400957 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800958 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000959 '-s',
960 '--isolated',
961 metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500962 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800963 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000964 '-e',
965 '--env',
966 default=[],
967 action='append',
968 nargs=2,
969 metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700970 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000972 '--env-prefix',
973 default=[],
974 action='append',
975 nargs=2,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800976 metavar='VAR local/path',
977 help='Prepend task-relative `local/path` to the task\'s VAR environment '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000978 'variable using os-appropriate pathsep character. Can be specified '
979 'multiple times for the same VAR to add multiple paths.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800980 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000981 '--idempotent',
982 action='store_true',
983 default=False,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400984 help='When set, the server will actively try to find a previous task '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000985 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800986 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000987 '--secret-bytes-path',
988 metavar='FILE',
Stephen Martinisf391c772019-02-01 01:22:12 +0000989 help='The optional path to a file containing the secret_bytes to use '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000990 'with this task.')
maruel681d6802017-01-17 16:56:03 -0800991 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000992 '--hard-timeout',
993 type='int',
994 default=60 * 60,
995 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400996 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800997 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000998 '--io-timeout',
999 type='int',
1000 default=20 * 60,
1001 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001002 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001003 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001004 '--lower-priority',
1005 action='store_true',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001006 help='Lowers the child process priority')
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001007 containment_choices = ('NONE', 'AUTO', 'JOB_OBJECT')
1008 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001009 '--containment-type',
1010 default='NONE',
1011 metavar='NONE',
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001012 choices=containment_choices,
1013 help='Containment to use; one of: %s' % ', '.join(containment_choices))
maruel681d6802017-01-17 16:56:03 -08001014 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001015 '--raw-cmd',
1016 action='store_true',
1017 default=False,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001018 help='When set, the command after -- is used as-is without run_isolated. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001019 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -08001020 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001021 '--relative-cwd',
1022 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001023 'requires --raw-cmd')
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001024 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001025 '--cipd-package',
1026 action='append',
1027 default=[],
1028 metavar='PKG',
maruel5475ba62017-05-31 15:35:47 -07001029 help='CIPD packages to install on the Swarming bot. Uses the format: '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001030 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001031 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001032 '--named-cache',
1033 action='append',
1034 nargs=2,
1035 default=[],
maruel5475ba62017-05-31 15:35:47 -07001036 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001037 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1038 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001039 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001040 help='Email of a service account to run the task as, or literal "bot" '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001041 'string to indicate that the task should use the same account the '
1042 'bot itself is using to authenticate to Swarming. Don\'t use task '
1043 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001044 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001045 '--pool-task-template',
1046 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1047 default='AUTO',
1048 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001049 'By default, the pool\'s TaskTemplate is automatically selected, '
1050 'according the pool configuration on the server. Choices are: '
1051 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
Robert Iannuccifafa7352018-06-13 17:08:17 +00001052 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001053 '-o',
1054 '--output',
1055 action='append',
1056 default=[],
1057 metavar='PATH',
maruel5475ba62017-05-31 15:35:47 -07001058 help='A list of files to return in addition to those written to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001059 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1060 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001061 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001062 '--wait-for-capacity',
1063 action='store_true',
1064 default=False,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001065 help='Instructs to leave the task PENDING even if there\'s no known bot '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001066 'that could run this task, otherwise the task will be denied with '
1067 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001068 parser.add_option_group(group)
1069
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001070 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001071 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001072 '--priority',
1073 type='int',
1074 default=200,
maruel681d6802017-01-17 16:56:03 -08001075 help='The lower value, the more important the task is')
1076 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001077 '-T',
1078 '--task-name',
1079 metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001080 help='Display name of the task. Defaults to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001081 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1082 'isolated file is provided, if a hash is provided, it defaults to '
1083 '<user>/<dimensions>/<isolated hash>/<timestamp>')
maruel681d6802017-01-17 16:56:03 -08001084 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001085 '--tags',
1086 action='append',
1087 default=[],
1088 metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001089 help='Tags to assign to the task.')
1090 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001091 '--user',
1092 default='',
maruel681d6802017-01-17 16:56:03 -08001093 help='User associated with the task. Defaults to authenticated user on '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001094 'the server.')
maruel681d6802017-01-17 16:56:03 -08001095 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001096 '--expiration',
1097 type='int',
1098 default=6 * 60 * 60,
1099 metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001100 help='Seconds to allow the task to be pending for a bot to run before '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001101 'this task request expires.')
maruel681d6802017-01-17 16:56:03 -08001102 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001103 '--deadline', type='int', dest='expiration', help=optparse.SUPPRESS_HELP)
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001104 group.add_option(
1105 '--realm',
1106 dest='realm',
1107 metavar='REALM',
1108 help='Realm associated with the task.')
Scott Lee44c13d72020-09-14 06:09:50 +00001109 group.add_option(
1110 '--resultdb',
1111 action='store_true',
1112 default=False,
1113 help='When set, the task is created with ResultDB enabled.')
maruel681d6802017-01-17 16:56:03 -08001114 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001115
1116
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001117def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001118 """Processes trigger options and does preparatory steps.
1119
1120 Returns:
1121 NewTaskRequest instance.
1122 """
maruelaf6b06c2017-06-08 06:26:53 -07001123 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001124 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001125 if args and args[0] == '--':
1126 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001127
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001128 if not options.dimensions:
1129 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001130 if not any(k == 'pool' for k, _v in options.dimensions):
1131 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001132 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1133 parser.error('--tags must be in the format key:value')
1134 if options.raw_cmd and not args:
1135 parser.error(
1136 'Arguments with --raw-cmd should be passed after -- as command '
1137 'delimiter.')
1138 if options.isolate_server and not options.namespace:
1139 parser.error(
1140 '--namespace must be a valid value when --isolate-server is used')
1141 if not options.isolated and not options.raw_cmd:
1142 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1143
1144 # Isolated
1145 # --isolated is required only if --raw-cmd wasn't provided.
1146 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1147 # preferred server.
Takuto Ikutaae767b32020-05-11 01:22:19 +00001148 isolateserver.process_isolate_server_options(parser, options,
1149 not options.raw_cmd)
maruel0a25f6c2017-05-10 10:43:23 -07001150 inputs_ref = None
1151 if options.isolate_server:
1152 inputs_ref = FilesRef(
1153 isolated=options.isolated,
1154 isolatedserver=options.isolate_server,
1155 namespace=options.namespace)
1156
1157 # Command
1158 command = None
1159 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001160 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001161 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001162 if options.relative_cwd:
1163 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1164 if not a.startswith(os.getcwd()):
1165 parser.error(
1166 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001167 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001168 if options.relative_cwd:
1169 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001170 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001171
maruel0a25f6c2017-05-10 10:43:23 -07001172 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001173 cipd_packages = []
1174 for p in options.cipd_package:
1175 split = p.split(':', 2)
1176 if len(split) != 3:
1177 parser.error('CIPD packages must take the form: path:package:version')
Junji Watanabe38b28b02020-04-23 10:23:30 +00001178 cipd_packages.append(
1179 CipdPackage(package_name=split[1], path=split[0], version=split[2]))
borenet02f772b2016-06-22 12:42:19 -07001180 cipd_input = None
1181 if cipd_packages:
1182 cipd_input = CipdInput(
Junji Watanabecb054042020-07-21 08:43:26 +00001183 client_package=None, packages=cipd_packages, server=None)
borenet02f772b2016-06-22 12:42:19 -07001184
maruel0a25f6c2017-05-10 10:43:23 -07001185 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001186 secret_bytes = None
1187 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001188 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001189 secret_bytes = f.read().encode('base64')
1190
maruel0a25f6c2017-05-10 10:43:23 -07001191 # Named caches
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001192 caches = [{
1193 u'name': six.text_type(i[0]),
1194 u'path': six.text_type(i[1])
1195 } for i in options.named_cache]
maruel0a25f6c2017-05-10 10:43:23 -07001196
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001197 env_prefixes = {}
1198 for k, v in options.env_prefix:
1199 env_prefixes.setdefault(k, []).append(v)
1200
Brad Hallf78187a2018-10-19 17:08:55 +00001201 # Get dimensions into the key/value format we can manipulate later.
Junji Watanabecb054042020-07-21 08:43:26 +00001202 orig_dims = [{
1203 'key': key,
1204 'value': value
1205 } for key, value in options.dimensions]
Brad Hallf78187a2018-10-19 17:08:55 +00001206 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1207
1208 # Construct base properties that we will use for all the slices, adding in
1209 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001210 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001211 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001212 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001213 command=command,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001214 containment=Containment(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001215 lower_priority=bool(options.lower_priority),
1216 containment_type=options.containment_type,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001217 ),
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001218 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001219 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001220 env=options.env,
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001221 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.items()],
maruel77f720b2015-09-15 12:35:22 -07001222 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001223 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001224 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001225 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001226 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001227 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001228 outputs=options.output,
1229 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001230
1231 slices = []
1232
1233 # Group the optional dimensions by expiration.
1234 dims_by_exp = {}
1235 for key, value, exp_secs in options.optional_dimensions:
Junji Watanabecb054042020-07-21 08:43:26 +00001236 dims_by_exp.setdefault(int(exp_secs), []).append({
1237 'key': key,
1238 'value': value
1239 })
Brad Hallf78187a2018-10-19 17:08:55 +00001240
1241 # Create the optional slices with expiration deltas, we fix up the properties
1242 # below.
1243 last_exp = 0
1244 for expiration_secs in sorted(dims_by_exp):
1245 t = TaskSlice(
1246 expiration_secs=expiration_secs - last_exp,
1247 properties=properties,
1248 wait_for_capacity=False)
1249 slices.append(t)
1250 last_exp = expiration_secs
1251
1252 # Add back in the default slice (the last one).
1253 exp = max(int(options.expiration) - last_exp, 60)
1254 base_task_slice = TaskSlice(
1255 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001256 properties=properties,
1257 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001258 slices.append(base_task_slice)
1259
Brad Hall7f463e62018-11-16 16:13:30 +00001260 # Add optional dimensions to the task slices, replacing a dimension that
1261 # has the same key if it is a dimension where repeating isn't valid (otherwise
1262 # we append it). Currently the only dimension we can repeat is "caches"; the
1263 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001264 extra_dims = []
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001265 for i, (_, kvs) in enumerate(sorted(dims_by_exp.items(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001266 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001267 # Replace or append the key/value pairs for this expiration in extra_dims;
1268 # we keep extra_dims around because we are iterating backwards and filling
1269 # in slices with shorter expirations. Dimensions expire as time goes on so
1270 # the slices that expire earlier will generally have more dimensions.
1271 for kv in kvs:
1272 if kv['key'] == 'caches':
1273 extra_dims.append(kv)
1274 else:
1275 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1276 # Then, add all the optional dimensions to the original dimension set, again
1277 # replacing if needed.
1278 for kv in extra_dims:
1279 if kv['key'] == 'caches':
1280 dims.append(kv)
1281 else:
1282 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001283 dims.sort(key=lambda x: (x['key'], x['value']))
1284 slice_properties = properties._replace(dimensions=dims)
1285 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1286
maruel77f720b2015-09-15 12:35:22 -07001287 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001288 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001289 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001290 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001291 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001292 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001293 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001294 user=options.user,
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001295 pool_task_template=options.pool_task_template,
Scott Lee44c13d72020-09-14 06:09:50 +00001296 realm=options.realm,
1297 resultdb={'enable': options.resultdb})
maruel@chromium.org0437a732013-08-27 16:05:52 +00001298
1299
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001300class TaskOutputStdoutOption(optparse.Option):
1301 """Where to output the each task's console output (stderr/stdout).
1302
1303 The output will be;
1304 none - not be downloaded.
1305 json - stored in summary.json file *only*.
1306 console - shown on stdout *only*.
1307 all - stored in summary.json and shown on stdout.
1308 """
1309
1310 choices = ['all', 'json', 'console', 'none']
1311
1312 def __init__(self, *args, **kw):
1313 optparse.Option.__init__(
1314 self,
1315 *args,
1316 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001317 default=['console', 'json'],
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001318 help=re.sub(r'\s\s*', ' ', self.__doc__),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001319 **kw)
1320
1321 def convert_value(self, opt, value):
1322 if value not in self.choices:
Junji Watanabecb054042020-07-21 08:43:26 +00001323 raise optparse.OptionValueError(
1324 "%s must be one of %s not %r" %
1325 (self.get_opt_string(), self.choices, value))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001326 stdout_to = []
1327 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001328 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001329 elif value != 'none':
1330 stdout_to = [value]
1331 return stdout_to
1332
1333
maruel@chromium.org0437a732013-08-27 16:05:52 +00001334def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001335 parser.server_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001336 '-t',
1337 '--timeout',
1338 type='float',
1339 default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001340 help='Timeout to wait for result, set to -1 for no timeout and get '
Junji Watanabecb054042020-07-21 08:43:26 +00001341 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001342 parser.group_logging.add_option(
1343 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001344 parser.group_logging.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001345 '--print-status-updates',
1346 action='store_true',
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001347 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001348 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001349 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001350 '--task-summary-json',
1351 metavar='FILE',
1352 help='Dump a summary of task results to this file as json. It contains '
Junji Watanabecb054042020-07-21 08:43:26 +00001353 'only shards statuses as know to server directly. Any output files '
1354 'emitted by the task can be collected by using --task-output-dir')
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001355 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001356 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001357 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001358 help='Directory to put task results into. When the task finishes, this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001359 'directory contains per-shard directory with output files produced '
1360 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001361 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001362 TaskOutputStdoutOption('--task-output-stdout'))
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001363 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001364 '--filepath-filter',
1365 help='This is regexp filter used to specify downloaded filepath when '
1366 'collecting isolated output.')
1367 parser.task_output_group.add_option(
1368 '--perf',
1369 action='store_true',
1370 default=False,
maruel9531ce02016-04-13 06:11:23 -07001371 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001372 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001373
1374
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001375def process_collect_options(parser, options):
1376 # Only negative -1 is allowed, disallow other negative values.
1377 if options.timeout != -1 and options.timeout < 0:
1378 parser.error('Invalid --timeout value')
1379
1380
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001381@subcommand.usage('bots...')
1382def CMDbot_delete(parser, args):
1383 """Forcibly deletes bots from the Swarming server."""
1384 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001385 '-f',
1386 '--force',
1387 action='store_true',
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001388 help='Do not prompt for confirmation')
1389 options, args = parser.parse_args(args)
1390 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001391 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001392
1393 bots = sorted(args)
1394 if not options.force:
1395 print('Delete the following bots?')
1396 for bot in bots:
1397 print(' %s' % bot)
1398 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1399 print('Goodbye.')
1400 return 1
1401
1402 result = 0
1403 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001404 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001405 if net.url_read_json(url, data={}, method='POST') is None:
1406 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001407 result = 1
1408 return result
1409
1410
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001411def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001412 """Returns information about the bots connected to the Swarming server."""
1413 add_filter_options(parser)
1414 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001415 '--dead-only',
1416 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001417 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001418 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001419 '-k',
1420 '--keep-dead',
1421 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001422 help='Keep both dead and alive bots')
1423 parser.filter_group.add_option(
1424 '--busy', action='store_true', help='Keep only busy bots')
1425 parser.filter_group.add_option(
1426 '--idle', action='store_true', help='Keep only idle bots')
1427 parser.filter_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001428 '--mp',
1429 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001430 help='Keep only Machine Provider managed bots')
1431 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001432 '--non-mp',
1433 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001434 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001435 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001436 '-b', '--bare', action='store_true', help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001437 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001438 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001439
1440 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001441 parser.error('Use only one of --keep-dead or --dead-only')
1442 if options.busy and options.idle:
1443 parser.error('Use only one of --busy or --idle')
1444 if options.mp and options.non_mp:
1445 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001446
smut281c3902018-05-30 17:50:05 -07001447 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001448 values = []
1449 if options.dead_only:
1450 values.append(('is_dead', 'TRUE'))
1451 elif options.keep_dead:
1452 values.append(('is_dead', 'NONE'))
1453 else:
1454 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001455
maruelaf6b06c2017-06-08 06:26:53 -07001456 if options.busy:
1457 values.append(('is_busy', 'TRUE'))
1458 elif options.idle:
1459 values.append(('is_busy', 'FALSE'))
1460 else:
1461 values.append(('is_busy', 'NONE'))
1462
1463 if options.mp:
1464 values.append(('is_mp', 'TRUE'))
1465 elif options.non_mp:
1466 values.append(('is_mp', 'FALSE'))
1467 else:
1468 values.append(('is_mp', 'NONE'))
1469
1470 for key, value in options.dimensions:
1471 values.append(('dimensions', '%s:%s' % (key, value)))
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +00001472 url += urllib.parse.urlencode(values)
maruelaf6b06c2017-06-08 06:26:53 -07001473 try:
1474 data, yielder = get_yielder(url, 0)
1475 bots = data.get('items') or []
1476 for items in yielder():
1477 if items:
1478 bots.extend(items)
1479 except Failure as e:
1480 sys.stderr.write('\n%s\n' % e)
1481 return 1
maruel77f720b2015-09-15 12:35:22 -07001482 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Lei Leife202df2019-06-11 17:33:34 +00001483 print(bot['bot_id'])
maruelaf6b06c2017-06-08 06:26:53 -07001484 if not options.bare:
1485 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Lei Leife202df2019-06-11 17:33:34 +00001486 print(' %s' % json.dumps(dimensions, sort_keys=True))
maruelaf6b06c2017-06-08 06:26:53 -07001487 if bot.get('task_id'):
Lei Leife202df2019-06-11 17:33:34 +00001488 print(' task: %s' % bot['task_id'])
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001489 return 0
1490
1491
maruelfd0a90c2016-06-10 11:51:10 -07001492@subcommand.usage('task_id')
1493def CMDcancel(parser, args):
1494 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001495 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001496 '-k',
1497 '--kill-running',
1498 action='store_true',
1499 default=False,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001500 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001501 options, args = parser.parse_args(args)
1502 if not args:
1503 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001504 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001505 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001506 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001507 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001508 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001509 print('Deleting %s failed. Probably already gone' % task_id)
1510 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001511 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001512 return 0
1513
1514
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001515@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001516def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001517 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001518
1519 The result can be in multiple part if the execution was sharded. It can
1520 potentially have retries.
1521 """
1522 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001523 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001524 '-j',
1525 '--json',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001526 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001527 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001528 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001529 if not args and not options.json:
1530 parser.error('Must specify at least one task id or --json.')
1531 if args and options.json:
1532 parser.error('Only use one of task id or --json.')
1533
1534 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001535 options.json = six.text_type(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001536 try:
maruel1ceb3872015-10-14 06:10:44 -07001537 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001538 data = json.load(f)
1539 except (IOError, ValueError):
1540 parser.error('Failed to open %s' % options.json)
1541 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001542 tasks = sorted(data['tasks'].values(), key=lambda x: x['shard_index'])
maruel71c61c82016-02-22 06:52:05 -08001543 args = [t['task_id'] for t in tasks]
1544 except (KeyError, TypeError):
1545 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001546 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001547 # Take in account all the task slices.
1548 offset = 0
1549 for s in data['request']['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +00001550 m = (
1551 offset + s['properties']['execution_timeout_secs'] +
1552 s['expiration_secs'])
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001553 if m > options.timeout:
1554 options.timeout = m
1555 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001556 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001557 else:
1558 valid = frozenset('0123456789abcdef')
1559 if any(not valid.issuperset(task_id) for task_id in args):
1560 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001561
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001562 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001563 return collect(options.swarming, args, options.timeout, options.decorate,
1564 options.print_status_updates, options.task_summary_json,
1565 options.task_output_dir, options.task_output_stdout,
1566 options.perf, options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001567 except Failure:
1568 on_error.report(None)
1569 return 1
1570
1571
maruel77f720b2015-09-15 12:35:22 -07001572@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001573def CMDpost(parser, args):
1574 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1575
1576 Input data must be sent to stdin, result is printed to stdout.
1577
1578 If HTTP response code >= 400, returns non-zero.
1579 """
1580 options, args = parser.parse_args(args)
1581 if len(args) != 1:
1582 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001583 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001584 data = sys.stdin.read()
1585 try:
1586 resp = net.url_read(url, data=data, method='POST')
1587 except net.TimeoutError:
1588 sys.stderr.write('Timeout!\n')
1589 return 1
1590 if not resp:
1591 sys.stderr.write('No response!\n')
1592 return 1
1593 sys.stdout.write(resp)
1594 return 0
1595
1596
1597@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001598def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001599 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1600 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001601
1602 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001603 Raw task request and results:
1604 swarming.py query -S server-url.com task/123456/request
1605 swarming.py query -S server-url.com task/123456/result
1606
maruel77f720b2015-09-15 12:35:22 -07001607 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001608 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001609
maruelaf6b06c2017-06-08 06:26:53 -07001610 Listing last 10 tasks on a specific bot named 'bot1':
1611 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001612
maruelaf6b06c2017-06-08 06:26:53 -07001613 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001614 quoting is important!:
1615 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001616 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001617 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001618 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001619 '-L',
1620 '--limit',
1621 type='int',
1622 default=200,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001623 help='Limit to enforce on limitless items (like number of tasks); '
Junji Watanabecb054042020-07-21 08:43:26 +00001624 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001625 parser.add_option(
1626 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001627 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001628 '--progress',
1629 action='store_true',
maruel77f720b2015-09-15 12:35:22 -07001630 help='Prints a dot at each request to show progress')
1631 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001632 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001633 parser.error(
1634 'Must specify only method name and optionally query args properly '
1635 'escaped.')
smut281c3902018-05-30 17:50:05 -07001636 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001637 try:
1638 data, yielder = get_yielder(base_url, options.limit)
1639 for items in yielder():
1640 if items:
1641 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001642 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001643 sys.stderr.write('.')
1644 sys.stderr.flush()
1645 except Failure as e:
1646 sys.stderr.write('\n%s\n' % e)
1647 return 1
maruel77f720b2015-09-15 12:35:22 -07001648 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001649 sys.stderr.write('\n')
1650 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001651 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001652 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001653 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001654 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001655 try:
maruel77f720b2015-09-15 12:35:22 -07001656 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001657 sys.stdout.write('\n')
1658 except IOError:
1659 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001660 return 0
1661
1662
maruel77f720b2015-09-15 12:35:22 -07001663def CMDquery_list(parser, args):
1664 """Returns list of all the Swarming APIs that can be used with command
1665 'query'.
1666 """
1667 parser.add_option(
1668 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1669 options, args = parser.parse_args(args)
1670 if args:
1671 parser.error('No argument allowed.')
1672
1673 try:
1674 apis = endpoints_api_discovery_apis(options.swarming)
1675 except APIError as e:
1676 parser.error(str(e))
1677 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001678 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001679 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001680 json.dump(apis, f)
1681 else:
1682 help_url = (
Junji Watanabecb054042020-07-21 08:43:26 +00001683 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1684 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001685 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -08001686 if i:
1687 print('')
Lei Leife202df2019-06-11 17:33:34 +00001688 print(api_id)
1689 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -08001690 if 'resources' in api:
1691 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001692 # TODO(maruel): Remove.
1693 # pylint: disable=too-many-nested-blocks
Junji Watanabecb054042020-07-21 08:43:26 +00001694 for j, (resource_name,
1695 resource) in enumerate(sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -08001696 if j:
1697 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001698 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -08001699 # Only list the GET ones.
1700 if method['httpMethod'] != 'GET':
1701 continue
Junji Watanabecb054042020-07-21 08:43:26 +00001702 print('- %s.%s: %s' % (resource_name, method_name, method['path']))
1703 print('\n'.join(' ' + l for l in textwrap.wrap(
1704 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +00001705 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -08001706 else:
1707 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001708 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -07001709 # Only list the GET ones.
1710 if method['httpMethod'] != 'GET':
1711 continue
Lei Leife202df2019-06-11 17:33:34 +00001712 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -08001713 print('\n'.join(
1714 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +00001715 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -07001716 return 0
1717
1718
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001719@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001720def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001721 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001722
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001723 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001724 """
1725 add_trigger_options(parser)
1726 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001727 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001728 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001729 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001730 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001731 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001732 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001733 except Failure as e:
Junji Watanabecb054042020-07-21 08:43:26 +00001734 on_error.report('Failed to trigger %s(%s): %s' %
1735 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001736 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001737 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001738 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001739 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001740 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001741 task_ids = [
Junji Watanabe38b28b02020-04-23 10:23:30 +00001742 t['task_id']
1743 for t in sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001744 ]
Caleb Rouleau779c4f02019-05-22 21:18:49 +00001745 for task_id in task_ids:
1746 print('Task: {server}/task?id={task}'.format(
1747 server=options.swarming, task=task_id))
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001748 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001749 offset = 0
1750 for s in task_request.task_slices:
Junji Watanabecb054042020-07-21 08:43:26 +00001751 m = (offset + s.properties.execution_timeout_secs + s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001752 if m > options.timeout:
1753 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001754 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001755 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001756 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001757 return collect(options.swarming, task_ids, options.timeout,
1758 options.decorate, options.print_status_updates,
1759 options.task_summary_json, options.task_output_dir,
1760 options.task_output_stdout, options.perf,
1761 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001762 except Failure:
1763 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001764 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001765
1766
maruel18122c62015-10-23 06:31:23 -07001767@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001768def CMDreproduce(parser, args):
1769 """Runs a task locally that was triggered on the server.
1770
1771 This running locally the same commands that have been run on the bot. The data
1772 downloaded will be in a subdirectory named 'work' of the current working
1773 directory.
maruel18122c62015-10-23 06:31:23 -07001774
1775 You can pass further additional arguments to the target command by passing
1776 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001777 """
maruelc070e672016-02-22 17:32:57 -08001778 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001779 '--output',
1780 metavar='DIR',
1781 default='out',
maruelc070e672016-02-22 17:32:57 -08001782 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001783 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001784 '--work',
1785 metavar='DIR',
1786 default='work',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001787 help='Directory to map the task input files into')
1788 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001789 '--cache',
1790 metavar='DIR',
1791 default='cache',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001792 help='Directory that contains the input cache')
1793 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001794 '--leak',
1795 action='store_true',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001796 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001797 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001798 extra_args = []
1799 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001800 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001801 if len(args) > 1:
1802 if args[1] == '--':
1803 if len(args) > 2:
1804 extra_args = args[2:]
1805 else:
1806 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001807
smut281c3902018-05-30 17:50:05 -07001808 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001809 request = net.url_read_json(url)
1810 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001811 print('Failed to retrieve request data for the task', file=sys.stderr)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001812 return 1
1813
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001814 workdir = six.text_type(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001815 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001816 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001817 fs.mkdir(workdir)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001818 cachedir = six.text_type(os.path.abspath('cipd_cache'))
iannucci31ab9192017-05-02 19:11:56 -07001819 if not fs.exists(cachedir):
1820 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001821
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001822 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001823 env = os.environ.copy()
1824 env['SWARMING_BOT_ID'] = 'reproduce'
1825 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001826 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001827 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001828 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001829 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001830 if not i['value']:
1831 env.pop(key, None)
1832 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001833 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001834
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001835 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001836 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001837 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001838 for i in env_prefixes:
1839 key = i['key']
1840 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001841 cur = env.get(key)
1842 if cur:
1843 paths.append(cur)
1844 env[key] = os.path.pathsep.join(paths)
1845
iannucci31ab9192017-05-02 19:11:56 -07001846 command = []
nodir152cba62016-05-12 16:08:56 -07001847 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001848 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001849 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +00001850 properties['inputs_ref']['isolatedserver'],
1851 properties['inputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001852 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001853 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1854 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1855 # leak.
1856 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001857 cache = local_caching.DiskContentAddressedCache(
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001858 six.text_type(os.path.abspath(options.cache)), policies, False)
maruel29ab2fd2015-10-16 11:44:01 -07001859 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001860 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001861 command = bundle.command
1862 if bundle.relative_cwd:
1863 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001864 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001865
1866 if properties.get('command'):
1867 command.extend(properties['command'])
1868
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001869 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Brian Sheedy7a761172019-08-30 22:55:14 +00001870 command = tools.find_executable(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001871 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001872 new_command = run_isolated.process_command(command, 'invalid', None)
1873 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001874 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001875 else:
1876 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001877 options.output = os.path.abspath(options.output)
Junji Watanabecb054042020-07-21 08:43:26 +00001878 new_command = run_isolated.process_command(command, options.output, None)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001879 if not os.path.isdir(options.output):
1880 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001881 command = new_command
1882 file_path.ensure_command_has_abs_path(command, workdir)
1883
1884 if properties.get('cipd_input'):
1885 ci = properties['cipd_input']
1886 cp = ci['client_package']
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001887 client_manager = cipd.get_client(cachedir, ci['server'], cp['package_name'],
1888 cp['version'])
iannucci31ab9192017-05-02 19:11:56 -07001889
1890 with client_manager as client:
1891 by_path = collections.defaultdict(list)
1892 for pkg in ci['packages']:
1893 path = pkg['path']
1894 # cipd deals with 'root' as ''
1895 if path == '.':
1896 path = ''
1897 by_path[path].append((pkg['package_name'], pkg['version']))
1898 client.ensure(workdir, by_path, cache_dir=cachedir)
1899
maruel77f720b2015-09-15 12:35:22 -07001900 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001901 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001902 except OSError as e:
Lei Leife202df2019-06-11 17:33:34 +00001903 print('Failed to run: %s' % ' '.join(command), file=sys.stderr)
1904 print(str(e), file=sys.stderr)
maruel77f720b2015-09-15 12:35:22 -07001905 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001906 finally:
1907 # Do not delete options.cache.
1908 if not options.leak:
1909 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001910
1911
maruel0eb1d1b2015-10-02 14:48:21 -07001912@subcommand.usage('bot_id')
1913def CMDterminate(parser, args):
1914 """Tells a bot to gracefully shut itself down as soon as it can.
1915
1916 This is done by completing whatever current task there is then exiting the bot
1917 process.
1918 """
1919 parser.add_option(
1920 '--wait', action='store_true', help='Wait for the bot to terminate')
1921 options, args = parser.parse_args(args)
1922 if len(args) != 1:
1923 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001924 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001925 request = net.url_read_json(url, data={})
1926 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001927 print('Failed to ask for termination', file=sys.stderr)
maruel0eb1d1b2015-10-02 14:48:21 -07001928 return 1
1929 if options.wait:
Junji Watanabecb054042020-07-21 08:43:26 +00001930 return collect(options.swarming, [request['task_id']], 0., False, False,
1931 None, None, [], False, None)
maruelbfc5f872017-06-10 16:43:17 -07001932 else:
Lei Leife202df2019-06-11 17:33:34 +00001933 print(request['task_id'])
maruel0eb1d1b2015-10-02 14:48:21 -07001934 return 0
1935
1936
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001937@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001938def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001939 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001940
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001941 Passes all extra arguments provided after '--' as additional command line
1942 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001943 """
1944 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001945 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001946 parser.add_option(
1947 '--dump-json',
1948 metavar='FILE',
1949 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001950 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001951 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001952 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001953 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001954 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001955 print('Triggered task: %s' % task_request.name)
Junji Watanabecb054042020-07-21 08:43:26 +00001956 tasks_sorted = sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001957 if options.dump_json:
1958 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001959 'base_task_name': task_request.name,
1960 'tasks': tasks,
1961 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001962 }
maruel46b015f2015-10-13 18:40:35 -07001963 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001964 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001965 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001966 (options.swarming, options.dump_json))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001967 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001968 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001969 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001970 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001971 print('Or visit:')
1972 for t in tasks_sorted:
1973 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001974 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001975 except Failure:
1976 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001977 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001978
1979
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001980class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +00001981
maruel@chromium.org0437a732013-08-27 16:05:52 +00001982 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001983 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001984 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001985 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001986 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001987 '-S',
1988 '--swarming',
1989 metavar='URL',
1990 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001991 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001992 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001993 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001994
1995 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001996 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001997 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001998 auth.process_auth_options(self, options)
1999 user = self._process_swarming(options)
2000 if hasattr(options, 'user') and not options.user:
2001 options.user = user
2002 return options, args
2003
2004 def _process_swarming(self, options):
2005 """Processes the --swarming option and aborts if not specified.
2006
2007 Returns the identity as determined by the server.
2008 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00002009 if not options.swarming:
2010 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002011 try:
2012 options.swarming = net.fix_url(options.swarming)
2013 except ValueError as e:
2014 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +00002015
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05002016 try:
2017 user = auth.ensure_logged_in(options.swarming)
2018 except ValueError as e:
2019 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002020 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00002021
2022
2023def main(args):
2024 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04002025 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002026
2027
2028if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07002029 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00002030 fix_encoding.fix_encoding()
2031 tools.disable_buffering()
2032 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +00002033 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002034 sys.exit(main(sys.argv[1:]))