blob: a6e5f3d2227db28b055ee6342e65d90ec11d0838 [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',
136 'task_slices',
137 'service_account',
138 'tags',
139 'user',
140 'pool_task_template',
141])
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500142
143
maruel77f720b2015-09-15 12:35:22 -0700144def namedtuple_to_dict(value):
145 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400146 if hasattr(value, '_asdict'):
147 return namedtuple_to_dict(value._asdict())
148 if isinstance(value, (list, tuple)):
149 return [namedtuple_to_dict(v) for v in value]
150 if isinstance(value, dict):
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000151 return {k: namedtuple_to_dict(v) for k, v in value.items()}
Lei Lei73a5f732020-03-23 20:36:14 +0000152 # json.dumps in Python3 doesn't support bytes.
153 if isinstance(value, bytes):
154 return six.ensure_str(value)
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400155 return value
maruel77f720b2015-09-15 12:35:22 -0700156
157
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700158def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800159 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700160
161 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500162 """
maruel77f720b2015-09-15 12:35:22 -0700163 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700164 # Don't send 'service_account' if it is None to avoid confusing older
165 # version of the server that doesn't know about 'service_account' and don't
166 # use it at all.
167 if not out['service_account']:
168 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000169 for task_slice in out['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +0000170 task_slice['properties']['env'] = [{
171 'key': k,
172 'value': v
173 } for k, v in task_slice['properties']['env'].items()]
Brad Hallf78187a2018-10-19 17:08:55 +0000174 task_slice['properties']['env'].sort(key=lambda x: x['key'])
Takuto Ikuta35250172020-01-31 09:33:46 +0000175 out['request_uuid'] = str(uuid.uuid4())
maruel77f720b2015-09-15 12:35:22 -0700176 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500177
178
maruel77f720b2015-09-15 12:35:22 -0700179def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500180 """Triggers a request on the Swarming server and returns the json data.
181
182 It's the low-level function.
183
184 Returns:
185 {
186 'request': {
187 'created_ts': u'2010-01-02 03:04:05',
188 'name': ..
189 },
190 'task_id': '12300',
191 }
192 """
193 logging.info('Triggering: %s', raw_request['name'])
194
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500195 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700196 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197 if not result:
198 on_error.report('Failed to trigger task %s' % raw_request['name'])
199 return None
maruele557bce2015-11-17 09:01:27 -0800200 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800201 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800202 msg = 'Failed to trigger task %s' % raw_request['name']
203 if result['error'].get('errors'):
204 for err in result['error']['errors']:
205 if err.get('message'):
206 msg += '\nMessage: %s' % err['message']
207 if err.get('debugInfo'):
208 msg += '\nDebug info:\n%s' % err['debugInfo']
209 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800210 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800211
212 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800213 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500214 return result
215
216
217def setup_googletest(env, shards, index):
218 """Sets googletest specific environment variables."""
219 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700220 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
221 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
222 env = env[:]
223 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
224 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500225 return env
226
227
228def trigger_task_shards(swarming, task_request, shards):
229 """Triggers one or many subtasks of a sharded task.
230
231 Returns:
232 Dict with task details, returned to caller as part of --dump-json output.
233 None in case of failure.
234 """
Junji Watanabecb054042020-07-21 08:43:26 +0000235
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500236 def convert(index):
Erik Chend50a88f2019-02-16 01:22:07 +0000237 """
238 Args:
239 index: The index of the task request.
240
241 Returns:
242 raw_request: A swarming compatible JSON dictionary of the request.
243 shard_index: The index of the shard, which may be different than the index
244 of the task request.
245 """
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700246 req = task_request_to_raw_request(task_request)
Erik Chend50a88f2019-02-16 01:22:07 +0000247 shard_index = index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 if shards > 1:
Brad Hall157bec82018-11-26 22:15:38 +0000249 for task_slice in req['task_slices']:
250 task_slice['properties']['env'] = setup_googletest(
251 task_slice['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700252 req['name'] += ':%s:%s' % (index, shards)
Erik Chend50a88f2019-02-16 01:22:07 +0000253 else:
254 task_slices = req['task_slices']
255
Lei Lei73a5f732020-03-23 20:36:14 +0000256 total_shards = 1
Erik Chend50a88f2019-02-16 01:22:07 +0000257 # Multiple tasks slices might exist if there are optional "slices", e.g.
258 # multiple ways of dispatching the task that should be equivalent. These
259 # should be functionally equivalent but we have cannot guarantee that. If
260 # we see the GTEST_SHARD_INDEX env var, we assume that it applies to all
261 # slices.
262 for task_slice in task_slices:
263 for env_var in task_slice['properties']['env']:
264 if env_var['key'] == 'GTEST_SHARD_INDEX':
265 shard_index = int(env_var['value'])
266 if env_var['key'] == 'GTEST_TOTAL_SHARDS':
267 total_shards = int(env_var['value'])
268 if total_shards > 1:
269 req['name'] += ':%s:%s' % (shard_index, total_shards)
Ben Pastened2a7be42020-07-14 22:28:55 +0000270 if shard_index and total_shards:
271 req['tags'] += [
272 'shard_index:%d' % shard_index,
273 'total_shards:%d' % total_shards,
274 ]
Erik Chend50a88f2019-02-16 01:22:07 +0000275
276 return req, shard_index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500277
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000278 requests = [convert(index) for index in range(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500279 tasks = {}
280 priority_warning = False
Erik Chend50a88f2019-02-16 01:22:07 +0000281 for request, shard_index in requests:
maruel77f720b2015-09-15 12:35:22 -0700282 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500283 if not task:
284 break
285 logging.info('Request result: %s', task)
286 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400287 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500288 priority_warning = True
Junji Watanabecb054042020-07-21 08:43:26 +0000289 print(
290 'Priority was reset to %s' % task['request']['priority'],
291 file=sys.stderr)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500292 tasks[request['name']] = {
Junji Watanabecb054042020-07-21 08:43:26 +0000293 'shard_index': shard_index,
294 'task_id': task['task_id'],
295 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500296 }
297
298 # Some shards weren't triggered. Abort everything.
299 if len(tasks) != len(requests):
300 if tasks:
Junji Watanabecb054042020-07-21 08:43:26 +0000301 print(
302 'Only %d shard(s) out of %d were triggered' %
303 (len(tasks), len(requests)),
304 file=sys.stderr)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000305 for task_dict in tasks.values():
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500306 abort_task(swarming, task_dict['task_id'])
307 return None
308
309 return tasks
310
311
312### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000313
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700314# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000315STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700316
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400317
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000318class TaskState(object):
319 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000320
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000321 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
322 is the source of truth for these values:
323 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400324
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000325 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400326 """
327 RUNNING = 0x10
328 PENDING = 0x20
329 EXPIRED = 0x30
330 TIMED_OUT = 0x40
331 BOT_DIED = 0x50
332 CANCELED = 0x60
333 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400334 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400335 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400336
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000337 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400338
maruel77f720b2015-09-15 12:35:22 -0700339 _ENUMS = {
Junji Watanabecb054042020-07-21 08:43:26 +0000340 'RUNNING': RUNNING,
341 'PENDING': PENDING,
342 'EXPIRED': EXPIRED,
343 'TIMED_OUT': TIMED_OUT,
344 'BOT_DIED': BOT_DIED,
345 'CANCELED': CANCELED,
346 'COMPLETED': COMPLETED,
347 'KILLED': KILLED,
348 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700349 }
350
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400351 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700352 def from_enum(cls, state):
353 """Returns int value based on the string."""
354 if state not in cls._ENUMS:
355 raise ValueError('Invalid state %s' % state)
356 return cls._ENUMS[state]
357
maruel@chromium.org0437a732013-08-27 16:05:52 +0000358
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700359class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700360 """Assembles task execution summary (for --task-summary-json output).
361
362 Optionally fetches task outputs from isolate server to local disk (used when
363 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364
365 This object is shared among multiple threads running 'retrieve_results'
366 function, in particular they call 'process_shard_result' method in parallel.
367 """
368
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000369 def __init__(self, task_output_dir, task_output_stdout, shard_count,
370 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700371 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
372
373 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700374 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700375 shard_count: expected number of task shards.
376 """
maruel12e30012015-10-09 11:55:35 -0700377 self.task_output_dir = (
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000378 six.text_type(os.path.abspath(task_output_dir))
maruel12e30012015-10-09 11:55:35 -0700379 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000380 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700381 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000382 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700383
384 self._lock = threading.Lock()
385 self._per_shard_results = {}
386 self._storage = None
387
nodire5028a92016-04-29 14:38:21 -0700388 if self.task_output_dir:
389 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700390
Vadim Shtayurab450c602014-05-12 19:23:25 -0700391 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700392 """Stores results of a single task shard, fetches output files if necessary.
393
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400394 Modifies |result| in place.
395
maruel77f720b2015-09-15 12:35:22 -0700396 shard_index is 0-based.
397
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700398 Called concurrently from multiple threads.
399 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700401 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 if shard_index < 0 or shard_index >= self.shard_count:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000403 logging.warning('Shard index %d is outside of expected range: [0; %d]',
404 shard_index, self.shard_count - 1)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700405 return
406
maruel77f720b2015-09-15 12:35:22 -0700407 if result.get('outputs_ref'):
408 ref = result['outputs_ref']
409 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
410 ref['isolatedserver'],
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000411 urllib.parse.urlencode([('namespace', ref['namespace']),
412 ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400413
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700414 # Store result dict of that shard, ignore results we've already seen.
415 with self._lock:
416 if shard_index in self._per_shard_results:
417 logging.warning('Ignoring duplicate shard index %d', shard_index)
418 return
419 self._per_shard_results[shard_index] = result
420
421 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700422 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000423 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +0000424 result['outputs_ref']['isolatedserver'],
425 result['outputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000426 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400427 if storage:
428 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400429 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
430 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400431 isolateserver.fetch_isolated(
Junji Watanabecb054042020-07-21 08:43:26 +0000432 result['outputs_ref']['isolated'], storage,
Lei Leife202df2019-06-11 17:33:34 +0000433 local_caching.MemoryContentAddressedCache(file_mode_mask=0o700),
Junji Watanabecb054042020-07-21 08:43:26 +0000434 os.path.join(self.task_output_dir, str(shard_index)), False,
435 self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700436
437 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700438 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439 with self._lock:
440 # Write an array of shard results with None for missing shards.
441 summary = {
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000442 'shards': [
443 self._per_shard_results.get(i) for i in range(self.shard_count)
444 ],
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000446
447 # Don't store stdout in the summary if not requested too.
448 if "json" not in self.task_output_stdout:
449 for shard_json in summary['shards']:
450 if not shard_json:
451 continue
452 if "output" in shard_json:
453 del shard_json["output"]
454 if "outputs" in shard_json:
455 del shard_json["outputs"]
456
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700457 # Write summary.json to task_output_dir as well.
458 if self.task_output_dir:
459 tools.write_json(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000460 os.path.join(self.task_output_dir, u'summary.json'), summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700461 if self._storage:
462 self._storage.close()
463 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700464 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700465
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000466 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700467 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700468 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700469 with self._lock:
470 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000471 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700472 else:
473 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000474 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700475 logging.error(
476 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000477 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700478 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000479 if self._storage.server_ref.namespace != server_ref.namespace:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000480 logging.error('Task shards are using multiple namespaces: %s and %s',
481 self._storage.server_ref.namespace,
482 server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700483 return None
484 return self._storage
485
486
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500487def now():
488 """Exists so it can be mocked easily."""
489 return time.time()
490
491
maruel77f720b2015-09-15 12:35:22 -0700492def parse_time(value):
493 """Converts serialized time from the API to datetime.datetime."""
494 # When microseconds are 0, the '.123456' suffix is elided. This means the
495 # serialized format is not consistent, which confuses the hell out of python.
496 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
497 try:
498 return datetime.datetime.strptime(value, fmt)
499 except ValueError:
500 pass
501 raise ValueError('Failed to parse %s' % value)
502
503
Junji Watanabe38b28b02020-04-23 10:23:30 +0000504def retrieve_results(base_url, shard_index, task_id, timeout, should_stop,
505 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400506 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700507
Vadim Shtayurab450c602014-05-12 19:23:25 -0700508 Returns:
509 <result dict> on success.
510 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700511 """
maruel71c61c82016-02-22 06:52:05 -0800512 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700513 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700514 if include_perf:
515 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700516 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700517 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400518 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700519 attempt = 0
520
521 while not should_stop.is_set():
522 attempt += 1
523
524 # Waiting for too long -> give up.
525 current_time = now()
526 if deadline and current_time >= deadline:
Junji Watanabecb054042020-07-21 08:43:26 +0000527 logging.error('retrieve_results(%s) timed out on attempt %d', base_url,
528 attempt)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700529 return None
530
531 # Do not spin too fast. Spin faster at the beginning though.
532 # Start with 1 sec delay and for each 30 sec of waiting add another second
533 # of delay, until hitting 15 sec ceiling.
534 if attempt > 1:
535 max_delay = min(15, 1 + (current_time - started) / 30.0)
536 delay = min(max_delay, deadline - current_time) if deadline else max_delay
537 if delay > 0:
538 logging.debug('Waiting %.1f sec before retrying', delay)
539 should_stop.wait(delay)
540 if should_stop.is_set():
541 return None
542
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400543 # Disable internal retries in net.url_read_json, since we are doing retries
544 # ourselves.
545 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700546 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
547 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400548 # Retry on 500s only if no timeout is specified.
549 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400550 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400551 if timeout == -1:
552 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400553 continue
maruel77f720b2015-09-15 12:35:22 -0700554
maruelbf53e042015-12-01 15:00:51 -0800555 if result.get('error'):
556 # An error occurred.
557 if result['error'].get('errors'):
558 for err in result['error']['errors']:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000559 logging.warning('Error while reading task: %s; %s',
560 err.get('message'), err.get('debugInfo'))
maruelbf53e042015-12-01 15:00:51 -0800561 elif result['error'].get('message'):
Junji Watanabecb054042020-07-21 08:43:26 +0000562 logging.warning('Error while reading task: %s',
563 result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400564 if timeout == -1:
565 return result
maruelbf53e042015-12-01 15:00:51 -0800566 continue
567
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400568 # When timeout == -1, always return on first attempt. 500s are already
569 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000570 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000571 if fetch_stdout:
572 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700573 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700574 # Record the result, try to fetch attached output files (if any).
575 if output_collector:
576 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700577 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700578 if result.get('internal_failure'):
579 logging.error('Internal error!')
580 elif result['state'] == 'BOT_DIED':
581 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700582 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000583
584
Junji Watanabecb054042020-07-21 08:43:26 +0000585def yield_results(swarm_base_url, task_ids, timeout, max_threads,
586 print_status_updates, output_collector, include_perf,
587 fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500588 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000589
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700590 Duplicate shards are ignored. Shards are yielded in order of completion.
591 Timed out shards are NOT yielded at all. Caller can compare number of yielded
592 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000593
594 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500595 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 +0000596 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500597
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700598 output_collector is an optional instance of TaskOutputCollector that will be
599 used to fetch files produced by a task from isolate server to the local disk.
600
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500601 Yields:
602 (index, result). In particular, 'result' is defined as the
603 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000604 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000605 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400606 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700607 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700608 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700609
maruel@chromium.org0437a732013-08-27 16:05:52 +0000610 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
611 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700612 # Adds a task to the thread pool to call 'retrieve_results' and return
613 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400614 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000615 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700616 task_fn = lambda *args: (shard_index, retrieve_results(*args))
Junji Watanabecb054042020-07-21 08:43:26 +0000617 pool.add_task(0, results_channel.wrap_task(task_fn), swarm_base_url,
618 shard_index, task_id, timeout, should_stop,
619 output_collector, include_perf, fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700620
621 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400622 for shard_index, task_id in enumerate(task_ids):
623 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700624
625 # Wait for all of them to finish.
Lei Lei73a5f732020-03-23 20:36:14 +0000626 # Convert to list, since range in Python3 doesn't have remove.
627 shards_remaining = list(range(len(task_ids)))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400628 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700629 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700630 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700631 try:
Marc-Antoine Ruel4494b6c2018-11-28 21:00:41 +0000632 shard_index, result = results_channel.next(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700634 except threading_utils.TaskChannel.Timeout:
635 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000636 time_now = str(datetime.datetime.now())
637 _, time_now = time_now.split(' ')
Junji Watanabe38b28b02020-04-23 10:23:30 +0000638 print('%s '
639 'Waiting for results from the following shards: %s' %
640 (time_now, ', '.join(map(str, shards_remaining))))
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700641 sys.stdout.flush()
642 continue
643 except Exception:
644 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700645
646 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700647 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000648 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500649 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700651
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652 # Yield back results to the caller.
653 assert shard_index in shards_remaining
654 shards_remaining.remove(shard_index)
655 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700656
maruel@chromium.org0437a732013-08-27 16:05:52 +0000657 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700658 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000659 should_stop.set()
660
661
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000662def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000663 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700664 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Junji Watanabecb054042020-07-21 08:43:26 +0000665 pending = '%.1fs' % (parse_time(metadata['started_ts']) -
666 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400667 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
668 metadata.get('abandoned_ts')):
Junji Watanabecb054042020-07-21 08:43:26 +0000669 pending = '%.1fs' % (parse_time(metadata['abandoned_ts']) -
670 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400671 else:
672 pending = 'N/A'
673
maruel77f720b2015-09-15 12:35:22 -0700674 if metadata.get('duration') is not None:
675 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400676 else:
677 duration = 'N/A'
678
maruel77f720b2015-09-15 12:35:22 -0700679 if metadata.get('exit_code') is not None:
680 # Integers are encoded as string to not loose precision.
681 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400682 else:
683 exit_code = 'N/A'
684
685 bot_id = metadata.get('bot_id') or 'N/A'
686
maruel77f720b2015-09-15 12:35:22 -0700687 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400688 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000689 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400690 if metadata.get('state') == 'CANCELED':
691 tag_footer2 = ' Pending: %s CANCELED' % pending
692 elif metadata.get('state') == 'EXPIRED':
693 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400694 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400695 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
696 pending, duration, bot_id, exit_code, metadata['state'])
697 else:
698 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
699 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400700
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000701 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
702 dash_pad = '+-%s-+' % ('-' * tag_len)
703 tag_header = '| %s |' % tag_header.ljust(tag_len)
704 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
705 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400706
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000707 if include_stdout:
708 return '\n'.join([
709 dash_pad,
710 tag_header,
711 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400712 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000713 dash_pad,
714 tag_footer1,
715 tag_footer2,
716 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000717 ])
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000718 return '\n'.join([
719 dash_pad,
720 tag_header,
721 tag_footer2,
722 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000723 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000724
725
Junji Watanabecb054042020-07-21 08:43:26 +0000726def collect(swarming, task_ids, timeout, decorate, print_status_updates,
727 task_summary_json, task_output_dir, task_output_stdout,
728 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700729 """Retrieves results of a Swarming task.
730
731 Returns:
732 process exit code that should be returned to the user.
733 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000734
735 filter_cb = None
736 if filepath_filter:
737 filter_cb = re.compile(filepath_filter).match
738
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700739 # Collect summary JSON and output files (if task_output_dir is not None).
Junji Watanabecb054042020-07-21 08:43:26 +0000740 output_collector = TaskOutputCollector(task_output_dir, task_output_stdout,
741 len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700742
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700743 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700744 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400745 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400747 for index, metadata in yield_results(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000748 swarming,
749 task_ids,
750 timeout,
751 None,
752 print_status_updates,
753 output_collector,
754 include_perf,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000755 (len(task_output_stdout) > 0),
Junji Watanabe38b28b02020-04-23 10:23:30 +0000756 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700757 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700758
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400759 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700760 shard_exit_code = metadata.get('exit_code')
761 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700762 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700763 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700764 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400765 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700766 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700767
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700768 if decorate:
Lei Lei73a5f732020-03-23 20:36:14 +0000769 # s is bytes in Python3, print could not print
770 # s with nice format, so decode s to str.
771 s = six.ensure_str(
772 decorate_shard_output(swarming, index, metadata,
773 "console" in task_output_stdout).encode(
774 'utf-8', 'replace'))
leileied181762016-10-13 14:24:59 -0700775 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400776 if len(seen_shards) < len(task_ids):
777 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700778 else:
Junji Watanabecb054042020-07-21 08:43:26 +0000779 print('%s: %s %s' % (metadata.get(
780 'bot_id', 'N/A'), metadata['task_id'], shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000781 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700782 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400783 if output:
784 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700785 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700786 summary = output_collector.finalize()
787 if task_summary_json:
788 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700789
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400790 if decorate and total_duration:
791 print('Total duration: %.1fs' % total_duration)
792
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400793 if len(seen_shards) != len(task_ids):
794 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Junji Watanabecb054042020-07-21 08:43:26 +0000795 print(
796 'Results from some shards are missing: %s' %
797 ', '.join(map(str, missing_shards)),
798 file=sys.stderr)
Vadim Shtayurac524f512014-05-15 09:54:56 -0700799 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700800
maruela5490782015-09-30 10:56:59 -0700801 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000802
803
maruel77f720b2015-09-15 12:35:22 -0700804### API management.
805
806
807class APIError(Exception):
808 pass
809
810
811def endpoints_api_discovery_apis(host):
812 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
813 the APIs exposed by a host.
814
815 https://developers.google.com/discovery/v1/reference/apis/list
816 """
maruel380e3262016-08-31 16:10:06 -0700817 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
818 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700819 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
820 if data is None:
821 raise APIError('Failed to discover APIs on %s' % host)
822 out = {}
823 for api in data['items']:
824 if api['id'] == 'discovery:v1':
825 continue
826 # URL is of the following form:
827 # url = host + (
828 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
829 api_data = net.url_read_json(api['discoveryRestUrl'])
830 if api_data is None:
831 raise APIError('Failed to discover %s on %s' % (api['id'], host))
832 out[api['id']] = api_data
833 return out
834
835
maruelaf6b06c2017-06-08 06:26:53 -0700836def get_yielder(base_url, limit):
837 """Returns the first query and a function that yields following items."""
838 CHUNK_SIZE = 250
839
840 url = base_url
841 if limit:
842 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
843 data = net.url_read_json(url)
844 if data is None:
845 # TODO(maruel): Do basic diagnostic.
846 raise Failure('Failed to access %s' % url)
847 org_cursor = data.pop('cursor', None)
848 org_total = len(data.get('items') or [])
849 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
850 if not org_cursor or not org_total:
851 # This is not an iterable resource.
852 return data, lambda: []
853
854 def yielder():
855 cursor = org_cursor
856 total = org_total
857 # Some items support cursors. Try to get automatically if cursors are needed
858 # by looking at the 'cursor' items.
859 while cursor and (not limit or total < limit):
860 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000861 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -0700862 if limit:
863 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
864 new = net.url_read_json(url)
865 if new is None:
866 raise Failure('Failed to access %s' % url)
867 cursor = new.get('cursor')
868 new_items = new.get('items')
869 nb_items = len(new_items or [])
870 total += nb_items
871 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
872 yield new_items
873
874 return data, yielder
875
876
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500877### Commands.
878
879
880def abort_task(_swarming, _manifest):
881 """Given a task manifest that was triggered, aborts its execution."""
882 # TODO(vadimsh): No supported by the server yet.
883
884
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400885def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800886 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500887 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000888 '-d',
889 '--dimension',
890 default=[],
891 action='append',
892 nargs=2,
893 dest='dimensions',
894 metavar='FOO bar',
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500895 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000896 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000897 '--optional-dimension',
898 default=[],
899 action='append',
900 nargs=3,
901 dest='optional_dimensions',
902 metavar='key value expiration',
Brad Hallf78187a2018-10-19 17:08:55 +0000903 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500904 parser.add_option_group(parser.filter_group)
905
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400906
Brad Hallf78187a2018-10-19 17:08:55 +0000907def _validate_filter_option(parser, key, value, expiration, argname):
908 if ':' in key:
909 parser.error('%s key cannot contain ":"' % argname)
910 if key.strip() != key:
911 parser.error('%s key has whitespace' % argname)
912 if not key:
913 parser.error('%s key is empty' % argname)
914
915 if value.strip() != value:
916 parser.error('%s value has whitespace' % argname)
917 if not value:
918 parser.error('%s value is empty' % argname)
919
920 if expiration is not None:
921 try:
922 expiration = int(expiration)
923 except ValueError:
924 parser.error('%s expiration is not an integer' % argname)
925 if expiration <= 0:
926 parser.error('%s expiration should be positive' % argname)
927 if expiration % 60 != 0:
928 parser.error('%s expiration is not divisible by 60' % argname)
929
930
maruelaf6b06c2017-06-08 06:26:53 -0700931def process_filter_options(parser, options):
932 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000933 _validate_filter_option(parser, key, value, None, 'dimension')
934 for key, value, exp in options.optional_dimensions:
935 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700936 options.dimensions.sort()
937
938
Vadim Shtayurab450c602014-05-12 19:23:25 -0700939def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400940 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700941 parser.sharding_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000942 '--shards',
943 type='int',
944 default=1,
945 metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700946 help='Number of shards to trigger and collect.')
947 parser.add_option_group(parser.sharding_group)
948
949
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400950def add_trigger_options(parser):
951 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500952 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400953 add_filter_options(parser)
954
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400955 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800956 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000957 '-s',
958 '--isolated',
959 metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500960 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800961 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000962 '-e',
963 '--env',
964 default=[],
965 action='append',
966 nargs=2,
967 metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700968 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800969 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000970 '--env-prefix',
971 default=[],
972 action='append',
973 nargs=2,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800974 metavar='VAR local/path',
975 help='Prepend task-relative `local/path` to the task\'s VAR environment '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000976 'variable using os-appropriate pathsep character. Can be specified '
977 'multiple times for the same VAR to add multiple paths.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800978 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000979 '--idempotent',
980 action='store_true',
981 default=False,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400982 help='When set, the server will actively try to find a previous task '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000983 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800984 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000985 '--secret-bytes-path',
986 metavar='FILE',
Stephen Martinisf391c772019-02-01 01:22:12 +0000987 help='The optional path to a file containing the secret_bytes to use '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000988 'with this task.')
maruel681d6802017-01-17 16:56:03 -0800989 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000990 '--hard-timeout',
991 type='int',
992 default=60 * 60,
993 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400994 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800995 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000996 '--io-timeout',
997 type='int',
998 default=20 * 60,
999 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001000 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001001 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001002 '--lower-priority',
1003 action='store_true',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001004 help='Lowers the child process priority')
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001005 containment_choices = ('NONE', 'AUTO', 'JOB_OBJECT')
1006 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001007 '--containment-type',
1008 default='NONE',
1009 metavar='NONE',
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001010 choices=containment_choices,
1011 help='Containment to use; one of: %s' % ', '.join(containment_choices))
maruel681d6802017-01-17 16:56:03 -08001012 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001013 '--raw-cmd',
1014 action='store_true',
1015 default=False,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001016 help='When set, the command after -- is used as-is without run_isolated. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001017 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -08001018 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001019 '--relative-cwd',
1020 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001021 'requires --raw-cmd')
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001022 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001023 '--cipd-package',
1024 action='append',
1025 default=[],
1026 metavar='PKG',
maruel5475ba62017-05-31 15:35:47 -07001027 help='CIPD packages to install on the Swarming bot. Uses the format: '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001028 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001029 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001030 '--named-cache',
1031 action='append',
1032 nargs=2,
1033 default=[],
maruel5475ba62017-05-31 15:35:47 -07001034 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001035 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1036 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001037 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001038 help='Email of a service account to run the task as, or literal "bot" '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001039 'string to indicate that the task should use the same account the '
1040 'bot itself is using to authenticate to Swarming. Don\'t use task '
1041 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001042 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001043 '--pool-task-template',
1044 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1045 default='AUTO',
1046 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001047 'By default, the pool\'s TaskTemplate is automatically selected, '
1048 'according the pool configuration on the server. Choices are: '
1049 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
Robert Iannuccifafa7352018-06-13 17:08:17 +00001050 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001051 '-o',
1052 '--output',
1053 action='append',
1054 default=[],
1055 metavar='PATH',
maruel5475ba62017-05-31 15:35:47 -07001056 help='A list of files to return in addition to those written to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001057 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1058 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001059 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001060 '--wait-for-capacity',
1061 action='store_true',
1062 default=False,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001063 help='Instructs to leave the task PENDING even if there\'s no known bot '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001064 'that could run this task, otherwise the task will be denied with '
1065 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001066 parser.add_option_group(group)
1067
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001068 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001069 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001070 '--priority',
1071 type='int',
1072 default=200,
maruel681d6802017-01-17 16:56:03 -08001073 help='The lower value, the more important the task is')
1074 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001075 '-T',
1076 '--task-name',
1077 metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001078 help='Display name of the task. Defaults to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001079 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1080 'isolated file is provided, if a hash is provided, it defaults to '
1081 '<user>/<dimensions>/<isolated hash>/<timestamp>')
maruel681d6802017-01-17 16:56:03 -08001082 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001083 '--tags',
1084 action='append',
1085 default=[],
1086 metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001087 help='Tags to assign to the task.')
1088 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001089 '--user',
1090 default='',
maruel681d6802017-01-17 16:56:03 -08001091 help='User associated with the task. Defaults to authenticated user on '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001092 'the server.')
maruel681d6802017-01-17 16:56:03 -08001093 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001094 '--expiration',
1095 type='int',
1096 default=6 * 60 * 60,
1097 metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001098 help='Seconds to allow the task to be pending for a bot to run before '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001099 'this task request expires.')
maruel681d6802017-01-17 16:56:03 -08001100 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001101 '--deadline', type='int', dest='expiration', help=optparse.SUPPRESS_HELP)
maruel681d6802017-01-17 16:56:03 -08001102 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001103
1104
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001105def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001106 """Processes trigger options and does preparatory steps.
1107
1108 Returns:
1109 NewTaskRequest instance.
1110 """
maruelaf6b06c2017-06-08 06:26:53 -07001111 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001112 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001113 if args and args[0] == '--':
1114 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001115
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001116 if not options.dimensions:
1117 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001118 if not any(k == 'pool' for k, _v in options.dimensions):
1119 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001120 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1121 parser.error('--tags must be in the format key:value')
1122 if options.raw_cmd and not args:
1123 parser.error(
1124 'Arguments with --raw-cmd should be passed after -- as command '
1125 'delimiter.')
1126 if options.isolate_server and not options.namespace:
1127 parser.error(
1128 '--namespace must be a valid value when --isolate-server is used')
1129 if not options.isolated and not options.raw_cmd:
1130 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1131
1132 # Isolated
1133 # --isolated is required only if --raw-cmd wasn't provided.
1134 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1135 # preferred server.
Takuto Ikutaae767b32020-05-11 01:22:19 +00001136 isolateserver.process_isolate_server_options(parser, options,
1137 not options.raw_cmd)
maruel0a25f6c2017-05-10 10:43:23 -07001138 inputs_ref = None
1139 if options.isolate_server:
1140 inputs_ref = FilesRef(
1141 isolated=options.isolated,
1142 isolatedserver=options.isolate_server,
1143 namespace=options.namespace)
1144
1145 # Command
1146 command = None
1147 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001148 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001149 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001150 if options.relative_cwd:
1151 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1152 if not a.startswith(os.getcwd()):
1153 parser.error(
1154 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001155 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001156 if options.relative_cwd:
1157 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001158 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001159
maruel0a25f6c2017-05-10 10:43:23 -07001160 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001161 cipd_packages = []
1162 for p in options.cipd_package:
1163 split = p.split(':', 2)
1164 if len(split) != 3:
1165 parser.error('CIPD packages must take the form: path:package:version')
Junji Watanabe38b28b02020-04-23 10:23:30 +00001166 cipd_packages.append(
1167 CipdPackage(package_name=split[1], path=split[0], version=split[2]))
borenet02f772b2016-06-22 12:42:19 -07001168 cipd_input = None
1169 if cipd_packages:
1170 cipd_input = CipdInput(
Junji Watanabecb054042020-07-21 08:43:26 +00001171 client_package=None, packages=cipd_packages, server=None)
borenet02f772b2016-06-22 12:42:19 -07001172
maruel0a25f6c2017-05-10 10:43:23 -07001173 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001174 secret_bytes = None
1175 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001176 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001177 secret_bytes = f.read().encode('base64')
1178
maruel0a25f6c2017-05-10 10:43:23 -07001179 # Named caches
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001180 caches = [{
1181 u'name': six.text_type(i[0]),
1182 u'path': six.text_type(i[1])
1183 } for i in options.named_cache]
maruel0a25f6c2017-05-10 10:43:23 -07001184
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001185 env_prefixes = {}
1186 for k, v in options.env_prefix:
1187 env_prefixes.setdefault(k, []).append(v)
1188
Brad Hallf78187a2018-10-19 17:08:55 +00001189 # Get dimensions into the key/value format we can manipulate later.
Junji Watanabecb054042020-07-21 08:43:26 +00001190 orig_dims = [{
1191 'key': key,
1192 'value': value
1193 } for key, value in options.dimensions]
Brad Hallf78187a2018-10-19 17:08:55 +00001194 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1195
1196 # Construct base properties that we will use for all the slices, adding in
1197 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001198 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001199 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001200 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001201 command=command,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001202 containment=Containment(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001203 lower_priority=bool(options.lower_priority),
1204 containment_type=options.containment_type,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001205 ),
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001206 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001207 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001208 env=options.env,
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001209 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.items()],
maruel77f720b2015-09-15 12:35:22 -07001210 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001211 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001212 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001213 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001214 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001215 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001216 outputs=options.output,
1217 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001218
1219 slices = []
1220
1221 # Group the optional dimensions by expiration.
1222 dims_by_exp = {}
1223 for key, value, exp_secs in options.optional_dimensions:
Junji Watanabecb054042020-07-21 08:43:26 +00001224 dims_by_exp.setdefault(int(exp_secs), []).append({
1225 'key': key,
1226 'value': value
1227 })
Brad Hallf78187a2018-10-19 17:08:55 +00001228
1229 # Create the optional slices with expiration deltas, we fix up the properties
1230 # below.
1231 last_exp = 0
1232 for expiration_secs in sorted(dims_by_exp):
1233 t = TaskSlice(
1234 expiration_secs=expiration_secs - last_exp,
1235 properties=properties,
1236 wait_for_capacity=False)
1237 slices.append(t)
1238 last_exp = expiration_secs
1239
1240 # Add back in the default slice (the last one).
1241 exp = max(int(options.expiration) - last_exp, 60)
1242 base_task_slice = TaskSlice(
1243 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001244 properties=properties,
1245 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001246 slices.append(base_task_slice)
1247
Brad Hall7f463e62018-11-16 16:13:30 +00001248 # Add optional dimensions to the task slices, replacing a dimension that
1249 # has the same key if it is a dimension where repeating isn't valid (otherwise
1250 # we append it). Currently the only dimension we can repeat is "caches"; the
1251 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001252 extra_dims = []
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001253 for i, (_, kvs) in enumerate(sorted(dims_by_exp.items(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001254 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001255 # Replace or append the key/value pairs for this expiration in extra_dims;
1256 # we keep extra_dims around because we are iterating backwards and filling
1257 # in slices with shorter expirations. Dimensions expire as time goes on so
1258 # the slices that expire earlier will generally have more dimensions.
1259 for kv in kvs:
1260 if kv['key'] == 'caches':
1261 extra_dims.append(kv)
1262 else:
1263 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1264 # Then, add all the optional dimensions to the original dimension set, again
1265 # replacing if needed.
1266 for kv in extra_dims:
1267 if kv['key'] == 'caches':
1268 dims.append(kv)
1269 else:
1270 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001271 dims.sort(key=lambda x: (x['key'], x['value']))
1272 slice_properties = properties._replace(dimensions=dims)
1273 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1274
maruel77f720b2015-09-15 12:35:22 -07001275 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001276 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001277 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001278 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001279 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001280 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001281 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001282 user=options.user,
1283 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001284
1285
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001286class TaskOutputStdoutOption(optparse.Option):
1287 """Where to output the each task's console output (stderr/stdout).
1288
1289 The output will be;
1290 none - not be downloaded.
1291 json - stored in summary.json file *only*.
1292 console - shown on stdout *only*.
1293 all - stored in summary.json and shown on stdout.
1294 """
1295
1296 choices = ['all', 'json', 'console', 'none']
1297
1298 def __init__(self, *args, **kw):
1299 optparse.Option.__init__(
1300 self,
1301 *args,
1302 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001303 default=['console', 'json'],
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001304 help=re.sub(r'\s\s*', ' ', self.__doc__),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001305 **kw)
1306
1307 def convert_value(self, opt, value):
1308 if value not in self.choices:
Junji Watanabecb054042020-07-21 08:43:26 +00001309 raise optparse.OptionValueError(
1310 "%s must be one of %s not %r" %
1311 (self.get_opt_string(), self.choices, value))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001312 stdout_to = []
1313 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001314 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001315 elif value != 'none':
1316 stdout_to = [value]
1317 return stdout_to
1318
1319
maruel@chromium.org0437a732013-08-27 16:05:52 +00001320def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001321 parser.server_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001322 '-t',
1323 '--timeout',
1324 type='float',
1325 default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001326 help='Timeout to wait for result, set to -1 for no timeout and get '
Junji Watanabecb054042020-07-21 08:43:26 +00001327 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001328 parser.group_logging.add_option(
1329 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001330 parser.group_logging.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001331 '--print-status-updates',
1332 action='store_true',
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001333 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001334 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001335 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001336 '--task-summary-json',
1337 metavar='FILE',
1338 help='Dump a summary of task results to this file as json. It contains '
Junji Watanabecb054042020-07-21 08:43:26 +00001339 'only shards statuses as know to server directly. Any output files '
1340 'emitted by the task can be collected by using --task-output-dir')
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001341 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001342 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001343 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001344 help='Directory to put task results into. When the task finishes, this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001345 'directory contains per-shard directory with output files produced '
1346 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001347 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001348 TaskOutputStdoutOption('--task-output-stdout'))
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001349 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001350 '--filepath-filter',
1351 help='This is regexp filter used to specify downloaded filepath when '
1352 'collecting isolated output.')
1353 parser.task_output_group.add_option(
1354 '--perf',
1355 action='store_true',
1356 default=False,
maruel9531ce02016-04-13 06:11:23 -07001357 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001358 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001359
1360
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001361def process_collect_options(parser, options):
1362 # Only negative -1 is allowed, disallow other negative values.
1363 if options.timeout != -1 and options.timeout < 0:
1364 parser.error('Invalid --timeout value')
1365
1366
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001367@subcommand.usage('bots...')
1368def CMDbot_delete(parser, args):
1369 """Forcibly deletes bots from the Swarming server."""
1370 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001371 '-f',
1372 '--force',
1373 action='store_true',
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001374 help='Do not prompt for confirmation')
1375 options, args = parser.parse_args(args)
1376 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001377 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001378
1379 bots = sorted(args)
1380 if not options.force:
1381 print('Delete the following bots?')
1382 for bot in bots:
1383 print(' %s' % bot)
1384 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1385 print('Goodbye.')
1386 return 1
1387
1388 result = 0
1389 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001390 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001391 if net.url_read_json(url, data={}, method='POST') is None:
1392 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001393 result = 1
1394 return result
1395
1396
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001397def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001398 """Returns information about the bots connected to the Swarming server."""
1399 add_filter_options(parser)
1400 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001401 '--dead-only',
1402 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001403 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001404 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001405 '-k',
1406 '--keep-dead',
1407 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001408 help='Keep both dead and alive bots')
1409 parser.filter_group.add_option(
1410 '--busy', action='store_true', help='Keep only busy bots')
1411 parser.filter_group.add_option(
1412 '--idle', action='store_true', help='Keep only idle bots')
1413 parser.filter_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001414 '--mp',
1415 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001416 help='Keep only Machine Provider managed bots')
1417 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001418 '--non-mp',
1419 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001420 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001421 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001422 '-b', '--bare', action='store_true', help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001423 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001424 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001425
1426 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001427 parser.error('Use only one of --keep-dead or --dead-only')
1428 if options.busy and options.idle:
1429 parser.error('Use only one of --busy or --idle')
1430 if options.mp and options.non_mp:
1431 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001432
smut281c3902018-05-30 17:50:05 -07001433 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001434 values = []
1435 if options.dead_only:
1436 values.append(('is_dead', 'TRUE'))
1437 elif options.keep_dead:
1438 values.append(('is_dead', 'NONE'))
1439 else:
1440 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001441
maruelaf6b06c2017-06-08 06:26:53 -07001442 if options.busy:
1443 values.append(('is_busy', 'TRUE'))
1444 elif options.idle:
1445 values.append(('is_busy', 'FALSE'))
1446 else:
1447 values.append(('is_busy', 'NONE'))
1448
1449 if options.mp:
1450 values.append(('is_mp', 'TRUE'))
1451 elif options.non_mp:
1452 values.append(('is_mp', 'FALSE'))
1453 else:
1454 values.append(('is_mp', 'NONE'))
1455
1456 for key, value in options.dimensions:
1457 values.append(('dimensions', '%s:%s' % (key, value)))
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +00001458 url += urllib.parse.urlencode(values)
maruelaf6b06c2017-06-08 06:26:53 -07001459 try:
1460 data, yielder = get_yielder(url, 0)
1461 bots = data.get('items') or []
1462 for items in yielder():
1463 if items:
1464 bots.extend(items)
1465 except Failure as e:
1466 sys.stderr.write('\n%s\n' % e)
1467 return 1
maruel77f720b2015-09-15 12:35:22 -07001468 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Lei Leife202df2019-06-11 17:33:34 +00001469 print(bot['bot_id'])
maruelaf6b06c2017-06-08 06:26:53 -07001470 if not options.bare:
1471 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Lei Leife202df2019-06-11 17:33:34 +00001472 print(' %s' % json.dumps(dimensions, sort_keys=True))
maruelaf6b06c2017-06-08 06:26:53 -07001473 if bot.get('task_id'):
Lei Leife202df2019-06-11 17:33:34 +00001474 print(' task: %s' % bot['task_id'])
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001475 return 0
1476
1477
maruelfd0a90c2016-06-10 11:51:10 -07001478@subcommand.usage('task_id')
1479def CMDcancel(parser, args):
1480 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001481 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001482 '-k',
1483 '--kill-running',
1484 action='store_true',
1485 default=False,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001486 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001487 options, args = parser.parse_args(args)
1488 if not args:
1489 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001490 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001491 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001492 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001493 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001494 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001495 print('Deleting %s failed. Probably already gone' % task_id)
1496 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001497 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001498 return 0
1499
1500
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001501@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001502def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001503 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001504
1505 The result can be in multiple part if the execution was sharded. It can
1506 potentially have retries.
1507 """
1508 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001509 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001510 '-j',
1511 '--json',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001512 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001513 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001514 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001515 if not args and not options.json:
1516 parser.error('Must specify at least one task id or --json.')
1517 if args and options.json:
1518 parser.error('Only use one of task id or --json.')
1519
1520 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001521 options.json = six.text_type(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001522 try:
maruel1ceb3872015-10-14 06:10:44 -07001523 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001524 data = json.load(f)
1525 except (IOError, ValueError):
1526 parser.error('Failed to open %s' % options.json)
1527 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001528 tasks = sorted(data['tasks'].values(), key=lambda x: x['shard_index'])
maruel71c61c82016-02-22 06:52:05 -08001529 args = [t['task_id'] for t in tasks]
1530 except (KeyError, TypeError):
1531 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001532 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001533 # Take in account all the task slices.
1534 offset = 0
1535 for s in data['request']['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +00001536 m = (
1537 offset + s['properties']['execution_timeout_secs'] +
1538 s['expiration_secs'])
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001539 if m > options.timeout:
1540 options.timeout = m
1541 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001542 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001543 else:
1544 valid = frozenset('0123456789abcdef')
1545 if any(not valid.issuperset(task_id) for task_id in args):
1546 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001547
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001548 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001549 return collect(options.swarming, args, options.timeout, options.decorate,
1550 options.print_status_updates, options.task_summary_json,
1551 options.task_output_dir, options.task_output_stdout,
1552 options.perf, options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001553 except Failure:
1554 on_error.report(None)
1555 return 1
1556
1557
maruel77f720b2015-09-15 12:35:22 -07001558@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001559def CMDpost(parser, args):
1560 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1561
1562 Input data must be sent to stdin, result is printed to stdout.
1563
1564 If HTTP response code >= 400, returns non-zero.
1565 """
1566 options, args = parser.parse_args(args)
1567 if len(args) != 1:
1568 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001569 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001570 data = sys.stdin.read()
1571 try:
1572 resp = net.url_read(url, data=data, method='POST')
1573 except net.TimeoutError:
1574 sys.stderr.write('Timeout!\n')
1575 return 1
1576 if not resp:
1577 sys.stderr.write('No response!\n')
1578 return 1
1579 sys.stdout.write(resp)
1580 return 0
1581
1582
1583@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001584def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001585 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1586 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001587
1588 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001589 Raw task request and results:
1590 swarming.py query -S server-url.com task/123456/request
1591 swarming.py query -S server-url.com task/123456/result
1592
maruel77f720b2015-09-15 12:35:22 -07001593 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001594 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001595
maruelaf6b06c2017-06-08 06:26:53 -07001596 Listing last 10 tasks on a specific bot named 'bot1':
1597 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001598
maruelaf6b06c2017-06-08 06:26:53 -07001599 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001600 quoting is important!:
1601 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001602 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001603 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001604 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001605 '-L',
1606 '--limit',
1607 type='int',
1608 default=200,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001609 help='Limit to enforce on limitless items (like number of tasks); '
Junji Watanabecb054042020-07-21 08:43:26 +00001610 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001611 parser.add_option(
1612 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001613 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001614 '--progress',
1615 action='store_true',
maruel77f720b2015-09-15 12:35:22 -07001616 help='Prints a dot at each request to show progress')
1617 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001618 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001619 parser.error(
1620 'Must specify only method name and optionally query args properly '
1621 'escaped.')
smut281c3902018-05-30 17:50:05 -07001622 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001623 try:
1624 data, yielder = get_yielder(base_url, options.limit)
1625 for items in yielder():
1626 if items:
1627 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001628 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001629 sys.stderr.write('.')
1630 sys.stderr.flush()
1631 except Failure as e:
1632 sys.stderr.write('\n%s\n' % e)
1633 return 1
maruel77f720b2015-09-15 12:35:22 -07001634 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001635 sys.stderr.write('\n')
1636 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001637 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001638 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001639 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001640 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001641 try:
maruel77f720b2015-09-15 12:35:22 -07001642 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001643 sys.stdout.write('\n')
1644 except IOError:
1645 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001646 return 0
1647
1648
maruel77f720b2015-09-15 12:35:22 -07001649def CMDquery_list(parser, args):
1650 """Returns list of all the Swarming APIs that can be used with command
1651 'query'.
1652 """
1653 parser.add_option(
1654 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1655 options, args = parser.parse_args(args)
1656 if args:
1657 parser.error('No argument allowed.')
1658
1659 try:
1660 apis = endpoints_api_discovery_apis(options.swarming)
1661 except APIError as e:
1662 parser.error(str(e))
1663 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001664 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001665 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001666 json.dump(apis, f)
1667 else:
1668 help_url = (
Junji Watanabecb054042020-07-21 08:43:26 +00001669 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1670 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001671 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -08001672 if i:
1673 print('')
Lei Leife202df2019-06-11 17:33:34 +00001674 print(api_id)
1675 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -08001676 if 'resources' in api:
1677 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001678 # TODO(maruel): Remove.
1679 # pylint: disable=too-many-nested-blocks
Junji Watanabecb054042020-07-21 08:43:26 +00001680 for j, (resource_name,
1681 resource) in enumerate(sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -08001682 if j:
1683 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001684 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -08001685 # Only list the GET ones.
1686 if method['httpMethod'] != 'GET':
1687 continue
Junji Watanabecb054042020-07-21 08:43:26 +00001688 print('- %s.%s: %s' % (resource_name, method_name, method['path']))
1689 print('\n'.join(' ' + l for l in textwrap.wrap(
1690 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +00001691 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -08001692 else:
1693 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001694 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -07001695 # Only list the GET ones.
1696 if method['httpMethod'] != 'GET':
1697 continue
Lei Leife202df2019-06-11 17:33:34 +00001698 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -08001699 print('\n'.join(
1700 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +00001701 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -07001702 return 0
1703
1704
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001705@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001706def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001707 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001708
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001709 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001710 """
1711 add_trigger_options(parser)
1712 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001713 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001714 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001715 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001716 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001717 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001718 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001719 except Failure as e:
Junji Watanabecb054042020-07-21 08:43:26 +00001720 on_error.report('Failed to trigger %s(%s): %s' %
1721 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001722 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001723 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001724 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001725 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001726 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001727 task_ids = [
Junji Watanabe38b28b02020-04-23 10:23:30 +00001728 t['task_id']
1729 for t in sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001730 ]
Caleb Rouleau779c4f02019-05-22 21:18:49 +00001731 for task_id in task_ids:
1732 print('Task: {server}/task?id={task}'.format(
1733 server=options.swarming, task=task_id))
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001734 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001735 offset = 0
1736 for s in task_request.task_slices:
Junji Watanabecb054042020-07-21 08:43:26 +00001737 m = (offset + s.properties.execution_timeout_secs + s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001738 if m > options.timeout:
1739 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001740 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001741 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001742 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001743 return collect(options.swarming, task_ids, options.timeout,
1744 options.decorate, options.print_status_updates,
1745 options.task_summary_json, options.task_output_dir,
1746 options.task_output_stdout, options.perf,
1747 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001748 except Failure:
1749 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001750 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001751
1752
maruel18122c62015-10-23 06:31:23 -07001753@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001754def CMDreproduce(parser, args):
1755 """Runs a task locally that was triggered on the server.
1756
1757 This running locally the same commands that have been run on the bot. The data
1758 downloaded will be in a subdirectory named 'work' of the current working
1759 directory.
maruel18122c62015-10-23 06:31:23 -07001760
1761 You can pass further additional arguments to the target command by passing
1762 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001763 """
maruelc070e672016-02-22 17:32:57 -08001764 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001765 '--output',
1766 metavar='DIR',
1767 default='out',
maruelc070e672016-02-22 17:32:57 -08001768 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001769 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001770 '--work',
1771 metavar='DIR',
1772 default='work',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001773 help='Directory to map the task input files into')
1774 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001775 '--cache',
1776 metavar='DIR',
1777 default='cache',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001778 help='Directory that contains the input cache')
1779 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001780 '--leak',
1781 action='store_true',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001782 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001783 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001784 extra_args = []
1785 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001786 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001787 if len(args) > 1:
1788 if args[1] == '--':
1789 if len(args) > 2:
1790 extra_args = args[2:]
1791 else:
1792 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001793
smut281c3902018-05-30 17:50:05 -07001794 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001795 request = net.url_read_json(url)
1796 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001797 print('Failed to retrieve request data for the task', file=sys.stderr)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001798 return 1
1799
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001800 workdir = six.text_type(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001801 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001802 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001803 fs.mkdir(workdir)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001804 cachedir = six.text_type(os.path.abspath('cipd_cache'))
iannucci31ab9192017-05-02 19:11:56 -07001805 if not fs.exists(cachedir):
1806 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001807
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001808 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001809 env = os.environ.copy()
1810 env['SWARMING_BOT_ID'] = 'reproduce'
1811 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001812 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001813 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001814 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001815 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001816 if not i['value']:
1817 env.pop(key, None)
1818 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001819 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001820
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001821 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001822 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001823 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001824 for i in env_prefixes:
1825 key = i['key']
1826 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001827 cur = env.get(key)
1828 if cur:
1829 paths.append(cur)
1830 env[key] = os.path.pathsep.join(paths)
1831
iannucci31ab9192017-05-02 19:11:56 -07001832 command = []
nodir152cba62016-05-12 16:08:56 -07001833 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001834 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001835 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +00001836 properties['inputs_ref']['isolatedserver'],
1837 properties['inputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001838 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001839 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1840 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1841 # leak.
1842 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001843 cache = local_caching.DiskContentAddressedCache(
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001844 six.text_type(os.path.abspath(options.cache)), policies, False)
maruel29ab2fd2015-10-16 11:44:01 -07001845 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001846 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001847 command = bundle.command
1848 if bundle.relative_cwd:
1849 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001850 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001851
1852 if properties.get('command'):
1853 command.extend(properties['command'])
1854
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001855 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Brian Sheedy7a761172019-08-30 22:55:14 +00001856 command = tools.find_executable(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001857 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001858 new_command = run_isolated.process_command(command, 'invalid', None)
1859 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001860 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001861 else:
1862 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001863 options.output = os.path.abspath(options.output)
Junji Watanabecb054042020-07-21 08:43:26 +00001864 new_command = run_isolated.process_command(command, options.output, None)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001865 if not os.path.isdir(options.output):
1866 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001867 command = new_command
1868 file_path.ensure_command_has_abs_path(command, workdir)
1869
1870 if properties.get('cipd_input'):
1871 ci = properties['cipd_input']
1872 cp = ci['client_package']
Junji Watanabecb054042020-07-21 08:43:26 +00001873 client_manager = cipd.get_client(ci['server'], cp['package_name'],
1874 cp['version'], cachedir)
iannucci31ab9192017-05-02 19:11:56 -07001875
1876 with client_manager as client:
1877 by_path = collections.defaultdict(list)
1878 for pkg in ci['packages']:
1879 path = pkg['path']
1880 # cipd deals with 'root' as ''
1881 if path == '.':
1882 path = ''
1883 by_path[path].append((pkg['package_name'], pkg['version']))
1884 client.ensure(workdir, by_path, cache_dir=cachedir)
1885
maruel77f720b2015-09-15 12:35:22 -07001886 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001887 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001888 except OSError as e:
Lei Leife202df2019-06-11 17:33:34 +00001889 print('Failed to run: %s' % ' '.join(command), file=sys.stderr)
1890 print(str(e), file=sys.stderr)
maruel77f720b2015-09-15 12:35:22 -07001891 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001892 finally:
1893 # Do not delete options.cache.
1894 if not options.leak:
1895 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001896
1897
maruel0eb1d1b2015-10-02 14:48:21 -07001898@subcommand.usage('bot_id')
1899def CMDterminate(parser, args):
1900 """Tells a bot to gracefully shut itself down as soon as it can.
1901
1902 This is done by completing whatever current task there is then exiting the bot
1903 process.
1904 """
1905 parser.add_option(
1906 '--wait', action='store_true', help='Wait for the bot to terminate')
1907 options, args = parser.parse_args(args)
1908 if len(args) != 1:
1909 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001910 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001911 request = net.url_read_json(url, data={})
1912 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001913 print('Failed to ask for termination', file=sys.stderr)
maruel0eb1d1b2015-10-02 14:48:21 -07001914 return 1
1915 if options.wait:
Junji Watanabecb054042020-07-21 08:43:26 +00001916 return collect(options.swarming, [request['task_id']], 0., False, False,
1917 None, None, [], False, None)
maruelbfc5f872017-06-10 16:43:17 -07001918 else:
Lei Leife202df2019-06-11 17:33:34 +00001919 print(request['task_id'])
maruel0eb1d1b2015-10-02 14:48:21 -07001920 return 0
1921
1922
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001923@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001924def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001925 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001926
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001927 Passes all extra arguments provided after '--' as additional command line
1928 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001929 """
1930 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001931 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001932 parser.add_option(
1933 '--dump-json',
1934 metavar='FILE',
1935 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001936 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001937 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001938 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001939 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001940 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001941 print('Triggered task: %s' % task_request.name)
Junji Watanabecb054042020-07-21 08:43:26 +00001942 tasks_sorted = sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001943 if options.dump_json:
1944 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001945 'base_task_name': task_request.name,
1946 'tasks': tasks,
1947 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001948 }
maruel46b015f2015-10-13 18:40:35 -07001949 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001950 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001951 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001952 (options.swarming, options.dump_json))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001953 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001954 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001955 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001956 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001957 print('Or visit:')
1958 for t in tasks_sorted:
1959 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001960 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001961 except Failure:
1962 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001963 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001964
1965
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001966class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +00001967
maruel@chromium.org0437a732013-08-27 16:05:52 +00001968 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001969 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001970 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001971 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001972 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001973 '-S',
1974 '--swarming',
1975 metavar='URL',
1976 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001977 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001978 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001979 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001980
1981 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001982 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001983 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001984 auth.process_auth_options(self, options)
1985 user = self._process_swarming(options)
1986 if hasattr(options, 'user') and not options.user:
1987 options.user = user
1988 return options, args
1989
1990 def _process_swarming(self, options):
1991 """Processes the --swarming option and aborts if not specified.
1992
1993 Returns the identity as determined by the server.
1994 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001995 if not options.swarming:
1996 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001997 try:
1998 options.swarming = net.fix_url(options.swarming)
1999 except ValueError as e:
2000 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +00002001
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05002002 try:
2003 user = auth.ensure_logged_in(options.swarming)
2004 except ValueError as e:
2005 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002006 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00002007
2008
2009def main(args):
2010 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04002011 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002012
2013
2014if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07002015 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00002016 fix_encoding.fix_encoding()
2017 tools.disable_buffering()
2018 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +00002019 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002020 sys.exit(main(sys.argv[1:]))