blob: f2549003d9fe8bf5428954de9d3fb65b43eead43 [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005"""Client tool to trigger tasks or retrieve results from a Swarming server."""
6
Lei Leife202df2019-06-11 17:33:34 +00007from __future__ import print_function
8
9__version__ = '1.0'
maruel@chromium.org0437a732013-08-27 16:05:52 +000010
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050011import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040012import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000013import json
14import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040015import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import os
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100017import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000018import sys
maruel11e31af2017-02-15 07:30:50 -080019import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070020import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000021import time
Takuto Ikuta35250172020-01-31 09:33:46 +000022import uuid
maruel@chromium.org0437a732013-08-27 16:05:52 +000023
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000024from utils import tools
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000025tools.force_local_third_party()
maruel@chromium.org0437a732013-08-27 16:05:52 +000026
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000027# third_party/
28import colorama
29from chromium import natsort
30from depot_tools import fix_encoding
31from depot_tools import subcommand
Takuto Ikuta6e2ff962019-10-29 12:35:27 +000032import six
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +000033from six.moves import urllib
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000034
35# pylint: disable=ungrouped-imports
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036import auth
iannucci31ab9192017-05-02 19:11:56 -070037import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000038import isolateserver
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +000039import isolate_storage
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040040import local_caching
maruelc070e672016-02-22 17:32:57 -080041import run_isolated
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000042from utils import file_path
43from utils import fs
44from utils import logging_utils
45from utils import net
46from utils import on_error
47from utils import subprocess42
48from utils import threading_utils
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050049
50
51class Failure(Exception):
52 """Generic failure."""
53 pass
54
55
maruel0a25f6c2017-05-10 10:43:23 -070056def default_task_name(options):
57 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050058 if not options.task_name:
Junji Watanabe38b28b02020-04-23 10:23:30 +000059 task_name = u'%s/%s' % (options.user, '_'.join(
60 '%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070061 if options.isolated:
62 task_name += u'/' + options.isolated
63 return task_name
64 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050065
66
67### Triggering.
68
maruel77f720b2015-09-15 12:35:22 -070069# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000070CipdPackage = collections.namedtuple('CipdPackage', [
71 'package_name',
72 'path',
73 'version',
74])
borenet02f772b2016-06-22 12:42:19 -070075
borenet02f772b2016-06-22 12:42:19 -070076# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000077CipdInput = collections.namedtuple('CipdInput', [
78 'client_package',
79 'packages',
80 'server',
81])
borenet02f772b2016-06-22 12:42:19 -070082
83# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000084FilesRef = collections.namedtuple('FilesRef', [
85 'isolated',
86 'isolatedserver',
87 'namespace',
88])
maruel77f720b2015-09-15 12:35:22 -070089
90# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080091StringListPair = collections.namedtuple(
Junji Watanabe38b28b02020-04-23 10:23:30 +000092 'StringListPair',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000093 [
Junji Watanabe38b28b02020-04-23 10:23:30 +000094 'key',
95 'value', # repeated string
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000096 ])
97
Junji Watanabe38b28b02020-04-23 10:23:30 +000098# See ../appengine/swarming/swarming_rpcs.py.
99Containment = collections.namedtuple('Containment', [
100 'lower_priority',
101 'containment_type',
102])
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800103
104# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +0000105TaskProperties = collections.namedtuple('TaskProperties', [
106 'caches',
107 'cipd_input',
108 'command',
109 'containment',
110 'relative_cwd',
111 'dimensions',
112 'env',
113 'env_prefixes',
114 'execution_timeout_secs',
115 'extra_args',
116 'grace_period_secs',
117 'idempotent',
118 'inputs_ref',
119 'io_timeout_secs',
120 'outputs',
121 'secret_bytes',
122])
maruel77f720b2015-09-15 12:35:22 -0700123
Junji Watanabecb054042020-07-21 08:43:26 +0000124# See ../appengine/swarming/swarming_rpcs.py.
125TaskSlice = collections.namedtuple('TaskSlice', [
126 'expiration_secs',
127 'properties',
128 'wait_for_capacity',
129])
maruel77f720b2015-09-15 12:35:22 -0700130
131# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabecb054042020-07-21 08:43:26 +0000132NewTaskRequest = collections.namedtuple('NewTaskRequest', [
133 'name',
134 'parent_task_id',
135 'priority',
Junji Watanabe71bbaef2020-07-21 08:55:37 +0000136 'realm',
Scott Lee44c13d72020-09-14 06:09:50 +0000137 'resultdb',
Junji Watanabecb054042020-07-21 08:43:26 +0000138 'task_slices',
139 'service_account',
140 'tags',
141 'user',
142 'pool_task_template',
143])
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144
145
maruel77f720b2015-09-15 12:35:22 -0700146def namedtuple_to_dict(value):
147 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400148 if hasattr(value, '_asdict'):
149 return namedtuple_to_dict(value._asdict())
150 if isinstance(value, (list, tuple)):
151 return [namedtuple_to_dict(v) for v in value]
152 if isinstance(value, dict):
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000153 return {k: namedtuple_to_dict(v) for k, v in value.items()}
Lei Lei73a5f732020-03-23 20:36:14 +0000154 # json.dumps in Python3 doesn't support bytes.
155 if isinstance(value, bytes):
156 return six.ensure_str(value)
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400157 return value
maruel77f720b2015-09-15 12:35:22 -0700158
159
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700160def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800161 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700162
163 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 """
maruel77f720b2015-09-15 12:35:22 -0700165 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700166 # Don't send 'service_account' if it is None to avoid confusing older
167 # version of the server that doesn't know about 'service_account' and don't
168 # use it at all.
169 if not out['service_account']:
170 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000171 for task_slice in out['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +0000172 task_slice['properties']['env'] = [{
173 'key': k,
174 'value': v
175 } for k, v in task_slice['properties']['env'].items()]
Brad Hallf78187a2018-10-19 17:08:55 +0000176 task_slice['properties']['env'].sort(key=lambda x: x['key'])
Takuto Ikuta35250172020-01-31 09:33:46 +0000177 out['request_uuid'] = str(uuid.uuid4())
maruel77f720b2015-09-15 12:35:22 -0700178 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500179
180
maruel77f720b2015-09-15 12:35:22 -0700181def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500182 """Triggers a request on the Swarming server and returns the json data.
183
184 It's the low-level function.
185
186 Returns:
187 {
188 'request': {
189 'created_ts': u'2010-01-02 03:04:05',
190 'name': ..
191 },
192 'task_id': '12300',
193 }
194 """
195 logging.info('Triggering: %s', raw_request['name'])
196
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700198 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199 if not result:
200 on_error.report('Failed to trigger task %s' % raw_request['name'])
201 return None
maruele557bce2015-11-17 09:01:27 -0800202 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800203 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800204 msg = 'Failed to trigger task %s' % raw_request['name']
205 if result['error'].get('errors'):
206 for err in result['error']['errors']:
207 if err.get('message'):
208 msg += '\nMessage: %s' % err['message']
209 if err.get('debugInfo'):
210 msg += '\nDebug info:\n%s' % err['debugInfo']
211 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800212 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800213
214 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800215 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500216 return result
217
218
219def setup_googletest(env, shards, index):
220 """Sets googletest specific environment variables."""
221 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700222 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
223 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
224 env = env[:]
225 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
226 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500227 return env
228
229
230def trigger_task_shards(swarming, task_request, shards):
231 """Triggers one or many subtasks of a sharded task.
232
233 Returns:
234 Dict with task details, returned to caller as part of --dump-json output.
235 None in case of failure.
236 """
Junji Watanabecb054042020-07-21 08:43:26 +0000237
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500238 def convert(index):
Erik Chend50a88f2019-02-16 01:22:07 +0000239 """
240 Args:
241 index: The index of the task request.
242
243 Returns:
244 raw_request: A swarming compatible JSON dictionary of the request.
245 shard_index: The index of the shard, which may be different than the index
246 of the task request.
247 """
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700248 req = task_request_to_raw_request(task_request)
Erik Chend50a88f2019-02-16 01:22:07 +0000249 shard_index = index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250 if shards > 1:
Brad Hall157bec82018-11-26 22:15:38 +0000251 for task_slice in req['task_slices']:
252 task_slice['properties']['env'] = setup_googletest(
253 task_slice['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700254 req['name'] += ':%s:%s' % (index, shards)
Erik Chend50a88f2019-02-16 01:22:07 +0000255 else:
256 task_slices = req['task_slices']
257
Lei Lei73a5f732020-03-23 20:36:14 +0000258 total_shards = 1
Erik Chend50a88f2019-02-16 01:22:07 +0000259 # Multiple tasks slices might exist if there are optional "slices", e.g.
260 # multiple ways of dispatching the task that should be equivalent. These
261 # should be functionally equivalent but we have cannot guarantee that. If
262 # we see the GTEST_SHARD_INDEX env var, we assume that it applies to all
263 # slices.
264 for task_slice in task_slices:
265 for env_var in task_slice['properties']['env']:
266 if env_var['key'] == 'GTEST_SHARD_INDEX':
267 shard_index = int(env_var['value'])
268 if env_var['key'] == 'GTEST_TOTAL_SHARDS':
269 total_shards = int(env_var['value'])
270 if total_shards > 1:
271 req['name'] += ':%s:%s' % (shard_index, total_shards)
Ben Pastened2a7be42020-07-14 22:28:55 +0000272 if shard_index and total_shards:
273 req['tags'] += [
274 'shard_index:%d' % shard_index,
275 'total_shards:%d' % total_shards,
276 ]
Erik Chend50a88f2019-02-16 01:22:07 +0000277
278 return req, shard_index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500279
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000280 requests = [convert(index) for index in range(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500281 tasks = {}
282 priority_warning = False
Erik Chend50a88f2019-02-16 01:22:07 +0000283 for request, shard_index in requests:
maruel77f720b2015-09-15 12:35:22 -0700284 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500285 if not task:
286 break
287 logging.info('Request result: %s', task)
288 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400289 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500290 priority_warning = True
Junji Watanabecb054042020-07-21 08:43:26 +0000291 print(
292 'Priority was reset to %s' % task['request']['priority'],
293 file=sys.stderr)
Ted Pudlikadc55e92020-09-28 23:25:49 +0000294 view_url = '%s/user/task/%s' % (swarming, task['task_id'])
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500295 tasks[request['name']] = {
Junji Watanabecb054042020-07-21 08:43:26 +0000296 'shard_index': shard_index,
297 'task_id': task['task_id'],
Ted Pudlikadc55e92020-09-28 23:25:49 +0000298 'view_url': view_url,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500299 }
Ted Pudlikadc55e92020-09-28 23:25:49 +0000300 logging.info('Task UI: %s', view_url)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500301
302 # Some shards weren't triggered. Abort everything.
303 if len(tasks) != len(requests):
304 if tasks:
Junji Watanabecb054042020-07-21 08:43:26 +0000305 print(
306 'Only %d shard(s) out of %d were triggered' %
307 (len(tasks), len(requests)),
308 file=sys.stderr)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000309 for task_dict in tasks.values():
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500310 abort_task(swarming, task_dict['task_id'])
311 return None
312
313 return tasks
314
315
316### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000317
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700318# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000319STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700320
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400321
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000322class TaskState(object):
323 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000324
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000325 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
326 is the source of truth for these values:
327 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400328
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000329 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400330 """
331 RUNNING = 0x10
332 PENDING = 0x20
333 EXPIRED = 0x30
334 TIMED_OUT = 0x40
335 BOT_DIED = 0x50
336 CANCELED = 0x60
337 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400338 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400339 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400340
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000341 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400342
maruel77f720b2015-09-15 12:35:22 -0700343 _ENUMS = {
Junji Watanabecb054042020-07-21 08:43:26 +0000344 'RUNNING': RUNNING,
345 'PENDING': PENDING,
346 'EXPIRED': EXPIRED,
347 'TIMED_OUT': TIMED_OUT,
348 'BOT_DIED': BOT_DIED,
349 'CANCELED': CANCELED,
350 'COMPLETED': COMPLETED,
351 'KILLED': KILLED,
352 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700353 }
354
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400355 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700356 def from_enum(cls, state):
357 """Returns int value based on the string."""
358 if state not in cls._ENUMS:
359 raise ValueError('Invalid state %s' % state)
360 return cls._ENUMS[state]
361
maruel@chromium.org0437a732013-08-27 16:05:52 +0000362
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700364 """Assembles task execution summary (for --task-summary-json output).
365
366 Optionally fetches task outputs from isolate server to local disk (used when
367 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368
369 This object is shared among multiple threads running 'retrieve_results'
370 function, in particular they call 'process_shard_result' method in parallel.
371 """
372
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000373 def __init__(self, task_output_dir, task_output_stdout, shard_count,
374 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700375 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
376
377 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700378 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700379 shard_count: expected number of task shards.
380 """
maruel12e30012015-10-09 11:55:35 -0700381 self.task_output_dir = (
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000382 six.text_type(os.path.abspath(task_output_dir))
maruel12e30012015-10-09 11:55:35 -0700383 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000384 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000386 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387
388 self._lock = threading.Lock()
389 self._per_shard_results = {}
390 self._storage = None
391
nodire5028a92016-04-29 14:38:21 -0700392 if self.task_output_dir:
393 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700394
Vadim Shtayurab450c602014-05-12 19:23:25 -0700395 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700396 """Stores results of a single task shard, fetches output files if necessary.
397
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400398 Modifies |result| in place.
399
maruel77f720b2015-09-15 12:35:22 -0700400 shard_index is 0-based.
401
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 Called concurrently from multiple threads.
403 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700404 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700405 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700406 if shard_index < 0 or shard_index >= self.shard_count:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000407 logging.warning('Shard index %d is outside of expected range: [0; %d]',
408 shard_index, self.shard_count - 1)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700409 return
410
maruel77f720b2015-09-15 12:35:22 -0700411 if result.get('outputs_ref'):
412 ref = result['outputs_ref']
413 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
414 ref['isolatedserver'],
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000415 urllib.parse.urlencode([('namespace', ref['namespace']),
416 ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400417
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700418 # Store result dict of that shard, ignore results we've already seen.
419 with self._lock:
420 if shard_index in self._per_shard_results:
421 logging.warning('Ignoring duplicate shard index %d', shard_index)
422 return
423 self._per_shard_results[shard_index] = result
424
425 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700426 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000427 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +0000428 result['outputs_ref']['isolatedserver'],
429 result['outputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000430 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400431 if storage:
432 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400433 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
434 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400435 isolateserver.fetch_isolated(
Junji Watanabecb054042020-07-21 08:43:26 +0000436 result['outputs_ref']['isolated'], storage,
Lei Leife202df2019-06-11 17:33:34 +0000437 local_caching.MemoryContentAddressedCache(file_mode_mask=0o700),
Junji Watanabecb054042020-07-21 08:43:26 +0000438 os.path.join(self.task_output_dir, str(shard_index)), False,
439 self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700440
441 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 with self._lock:
444 # Write an array of shard results with None for missing shards.
445 summary = {
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000446 'shards': [
447 self._per_shard_results.get(i) for i in range(self.shard_count)
448 ],
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700449 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000450
451 # Don't store stdout in the summary if not requested too.
452 if "json" not in self.task_output_stdout:
453 for shard_json in summary['shards']:
454 if not shard_json:
455 continue
456 if "output" in shard_json:
457 del shard_json["output"]
458 if "outputs" in shard_json:
459 del shard_json["outputs"]
460
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700461 # Write summary.json to task_output_dir as well.
462 if self.task_output_dir:
463 tools.write_json(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000464 os.path.join(self.task_output_dir, u'summary.json'), summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700465 if self._storage:
466 self._storage.close()
467 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700468 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700469
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000470 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700471 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700472 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700473 with self._lock:
474 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000475 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700476 else:
477 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000478 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700479 logging.error(
480 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000481 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700482 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000483 if self._storage.server_ref.namespace != server_ref.namespace:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000484 logging.error('Task shards are using multiple namespaces: %s and %s',
485 self._storage.server_ref.namespace,
486 server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700487 return None
488 return self._storage
489
490
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500491def now():
492 """Exists so it can be mocked easily."""
493 return time.time()
494
495
maruel77f720b2015-09-15 12:35:22 -0700496def parse_time(value):
497 """Converts serialized time from the API to datetime.datetime."""
498 # When microseconds are 0, the '.123456' suffix is elided. This means the
499 # serialized format is not consistent, which confuses the hell out of python.
500 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
501 try:
502 return datetime.datetime.strptime(value, fmt)
503 except ValueError:
504 pass
505 raise ValueError('Failed to parse %s' % value)
506
507
Junji Watanabe38b28b02020-04-23 10:23:30 +0000508def retrieve_results(base_url, shard_index, task_id, timeout, should_stop,
509 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400510 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700511
Vadim Shtayurab450c602014-05-12 19:23:25 -0700512 Returns:
513 <result dict> on success.
514 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700515 """
maruel71c61c82016-02-22 06:52:05 -0800516 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700517 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700518 if include_perf:
519 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700520 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700521 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400522 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700523 attempt = 0
524
525 while not should_stop.is_set():
526 attempt += 1
527
528 # Waiting for too long -> give up.
529 current_time = now()
530 if deadline and current_time >= deadline:
Junji Watanabecb054042020-07-21 08:43:26 +0000531 logging.error('retrieve_results(%s) timed out on attempt %d', base_url,
532 attempt)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700533 return None
534
535 # Do not spin too fast. Spin faster at the beginning though.
536 # Start with 1 sec delay and for each 30 sec of waiting add another second
537 # of delay, until hitting 15 sec ceiling.
538 if attempt > 1:
539 max_delay = min(15, 1 + (current_time - started) / 30.0)
540 delay = min(max_delay, deadline - current_time) if deadline else max_delay
541 if delay > 0:
542 logging.debug('Waiting %.1f sec before retrying', delay)
543 should_stop.wait(delay)
544 if should_stop.is_set():
545 return None
546
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400547 # Disable internal retries in net.url_read_json, since we are doing retries
548 # ourselves.
549 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700550 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
551 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400552 # Retry on 500s only if no timeout is specified.
553 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400554 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400555 if timeout == -1:
556 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400557 continue
maruel77f720b2015-09-15 12:35:22 -0700558
maruelbf53e042015-12-01 15:00:51 -0800559 if result.get('error'):
560 # An error occurred.
561 if result['error'].get('errors'):
562 for err in result['error']['errors']:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000563 logging.warning('Error while reading task: %s; %s',
564 err.get('message'), err.get('debugInfo'))
maruelbf53e042015-12-01 15:00:51 -0800565 elif result['error'].get('message'):
Junji Watanabecb054042020-07-21 08:43:26 +0000566 logging.warning('Error while reading task: %s',
567 result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400568 if timeout == -1:
569 return result
maruelbf53e042015-12-01 15:00:51 -0800570 continue
571
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400572 # When timeout == -1, always return on first attempt. 500s are already
573 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000574 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000575 if fetch_stdout:
576 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700577 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700578 # Record the result, try to fetch attached output files (if any).
579 if output_collector:
580 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700581 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700582 if result.get('internal_failure'):
583 logging.error('Internal error!')
584 elif result['state'] == 'BOT_DIED':
585 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700586 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000587
588
Junji Watanabecb054042020-07-21 08:43:26 +0000589def yield_results(swarm_base_url, task_ids, timeout, max_threads,
590 print_status_updates, output_collector, include_perf,
591 fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500592 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000593
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700594 Duplicate shards are ignored. Shards are yielded in order of completion.
595 Timed out shards are NOT yielded at all. Caller can compare number of yielded
596 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000597
598 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500599 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 +0000600 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500601
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700602 output_collector is an optional instance of TaskOutputCollector that will be
603 used to fetch files produced by a task from isolate server to the local disk.
604
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500605 Yields:
606 (index, result). In particular, 'result' is defined as the
607 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000608 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000609 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400610 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700611 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700612 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700613
maruel@chromium.org0437a732013-08-27 16:05:52 +0000614 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
615 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700616 # Adds a task to the thread pool to call 'retrieve_results' and return
617 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400618 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000619 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700620 task_fn = lambda *args: (shard_index, retrieve_results(*args))
Junji Watanabecb054042020-07-21 08:43:26 +0000621 pool.add_task(0, results_channel.wrap_task(task_fn), swarm_base_url,
622 shard_index, task_id, timeout, should_stop,
623 output_collector, include_perf, fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700624
625 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400626 for shard_index, task_id in enumerate(task_ids):
627 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700628
629 # Wait for all of them to finish.
Lei Lei73a5f732020-03-23 20:36:14 +0000630 # Convert to list, since range in Python3 doesn't have remove.
631 shards_remaining = list(range(len(task_ids)))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400632 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700633 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700635 try:
Marc-Antoine Ruel4494b6c2018-11-28 21:00:41 +0000636 shard_index, result = results_channel.next(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700637 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700638 except threading_utils.TaskChannel.Timeout:
639 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000640 time_now = str(datetime.datetime.now())
641 _, time_now = time_now.split(' ')
Junji Watanabe38b28b02020-04-23 10:23:30 +0000642 print('%s '
643 'Waiting for results from the following shards: %s' %
644 (time_now, ', '.join(map(str, shards_remaining))))
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700645 sys.stdout.flush()
646 continue
647 except Exception:
648 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700649
650 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700651 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000652 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500653 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000654 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700655
Vadim Shtayurab450c602014-05-12 19:23:25 -0700656 # Yield back results to the caller.
657 assert shard_index in shards_remaining
658 shards_remaining.remove(shard_index)
659 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700660
maruel@chromium.org0437a732013-08-27 16:05:52 +0000661 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700662 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000663 should_stop.set()
664
665
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000666def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000667 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700668 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Junji Watanabecb054042020-07-21 08:43:26 +0000669 pending = '%.1fs' % (parse_time(metadata['started_ts']) -
670 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400671 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
672 metadata.get('abandoned_ts')):
Junji Watanabecb054042020-07-21 08:43:26 +0000673 pending = '%.1fs' % (parse_time(metadata['abandoned_ts']) -
674 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400675 else:
676 pending = 'N/A'
677
maruel77f720b2015-09-15 12:35:22 -0700678 if metadata.get('duration') is not None:
679 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400680 else:
681 duration = 'N/A'
682
maruel77f720b2015-09-15 12:35:22 -0700683 if metadata.get('exit_code') is not None:
684 # Integers are encoded as string to not loose precision.
685 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400686 else:
687 exit_code = 'N/A'
688
689 bot_id = metadata.get('bot_id') or 'N/A'
690
maruel77f720b2015-09-15 12:35:22 -0700691 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400692 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000693 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400694 if metadata.get('state') == 'CANCELED':
695 tag_footer2 = ' Pending: %s CANCELED' % pending
696 elif metadata.get('state') == 'EXPIRED':
697 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400698 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400699 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
700 pending, duration, bot_id, exit_code, metadata['state'])
701 else:
702 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
703 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400704
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000705 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
706 dash_pad = '+-%s-+' % ('-' * tag_len)
707 tag_header = '| %s |' % tag_header.ljust(tag_len)
708 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
709 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400710
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000711 if include_stdout:
712 return '\n'.join([
713 dash_pad,
714 tag_header,
715 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400716 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000717 dash_pad,
718 tag_footer1,
719 tag_footer2,
720 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000721 ])
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000722 return '\n'.join([
723 dash_pad,
724 tag_header,
725 tag_footer2,
726 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000727 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000728
729
Junji Watanabecb054042020-07-21 08:43:26 +0000730def collect(swarming, task_ids, timeout, decorate, print_status_updates,
731 task_summary_json, task_output_dir, task_output_stdout,
732 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700733 """Retrieves results of a Swarming task.
734
735 Returns:
736 process exit code that should be returned to the user.
737 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000738
739 filter_cb = None
740 if filepath_filter:
741 filter_cb = re.compile(filepath_filter).match
742
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700743 # Collect summary JSON and output files (if task_output_dir is not None).
Junji Watanabecb054042020-07-21 08:43:26 +0000744 output_collector = TaskOutputCollector(task_output_dir, task_output_stdout,
745 len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700747 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700748 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400749 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700750 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400751 for index, metadata in yield_results(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000752 swarming,
753 task_ids,
754 timeout,
755 None,
756 print_status_updates,
757 output_collector,
758 include_perf,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000759 (len(task_output_stdout) > 0),
Junji Watanabe38b28b02020-04-23 10:23:30 +0000760 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700761 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700762
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400763 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700764 shard_exit_code = metadata.get('exit_code')
765 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700766 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700767 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700768 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400769 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700770 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700771
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700772 if decorate:
Lei Lei805a75d2020-10-08 16:31:55 +0000773 s = decorate_shard_output(swarming, index, metadata,
Lei Lei73a5f732020-03-23 20:36:14 +0000774 "console" in task_output_stdout).encode(
Lei Lei805a75d2020-10-08 16:31:55 +0000775 'utf-8', 'replace')
776
777 # The default system encoding is ascii, which can not handle non-ascii
778 # characters, switch to use sys.stdout.buffer.write in Python3 to
779 # send utf-8 to stdout regardless of the console's encoding.
780 if six.PY3:
781 sys.stdout.buffer.write(s)
782 else:
783 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400784 if len(seen_shards) < len(task_ids):
785 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700786 else:
Junji Watanabecb054042020-07-21 08:43:26 +0000787 print('%s: %s %s' % (metadata.get(
788 'bot_id', 'N/A'), metadata['task_id'], shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000789 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700790 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400791 if output:
792 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700793 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700794 summary = output_collector.finalize()
795 if task_summary_json:
796 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700797
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400798 if decorate and total_duration:
799 print('Total duration: %.1fs' % total_duration)
800
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400801 if len(seen_shards) != len(task_ids):
802 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Junji Watanabecb054042020-07-21 08:43:26 +0000803 print(
804 'Results from some shards are missing: %s' %
805 ', '.join(map(str, missing_shards)),
806 file=sys.stderr)
Vadim Shtayurac524f512014-05-15 09:54:56 -0700807 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700808
maruela5490782015-09-30 10:56:59 -0700809 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000810
811
maruel77f720b2015-09-15 12:35:22 -0700812### API management.
813
814
815class APIError(Exception):
816 pass
817
818
819def endpoints_api_discovery_apis(host):
820 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
821 the APIs exposed by a host.
822
823 https://developers.google.com/discovery/v1/reference/apis/list
824 """
maruel380e3262016-08-31 16:10:06 -0700825 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
826 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700827 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
828 if data is None:
829 raise APIError('Failed to discover APIs on %s' % host)
830 out = {}
831 for api in data['items']:
832 if api['id'] == 'discovery:v1':
833 continue
834 # URL is of the following form:
835 # url = host + (
836 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
837 api_data = net.url_read_json(api['discoveryRestUrl'])
838 if api_data is None:
839 raise APIError('Failed to discover %s on %s' % (api['id'], host))
840 out[api['id']] = api_data
841 return out
842
843
maruelaf6b06c2017-06-08 06:26:53 -0700844def get_yielder(base_url, limit):
845 """Returns the first query and a function that yields following items."""
846 CHUNK_SIZE = 250
847
848 url = base_url
849 if limit:
850 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
851 data = net.url_read_json(url)
852 if data is None:
853 # TODO(maruel): Do basic diagnostic.
854 raise Failure('Failed to access %s' % url)
855 org_cursor = data.pop('cursor', None)
856 org_total = len(data.get('items') or [])
857 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
858 if not org_cursor or not org_total:
859 # This is not an iterable resource.
860 return data, lambda: []
861
862 def yielder():
863 cursor = org_cursor
864 total = org_total
865 # Some items support cursors. Try to get automatically if cursors are needed
866 # by looking at the 'cursor' items.
867 while cursor and (not limit or total < limit):
868 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000869 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -0700870 if limit:
871 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
872 new = net.url_read_json(url)
873 if new is None:
874 raise Failure('Failed to access %s' % url)
875 cursor = new.get('cursor')
876 new_items = new.get('items')
877 nb_items = len(new_items or [])
878 total += nb_items
879 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
880 yield new_items
881
882 return data, yielder
883
884
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500885### Commands.
886
887
888def abort_task(_swarming, _manifest):
889 """Given a task manifest that was triggered, aborts its execution."""
890 # TODO(vadimsh): No supported by the server yet.
891
892
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400893def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800894 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500895 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000896 '-d',
897 '--dimension',
898 default=[],
899 action='append',
900 nargs=2,
901 dest='dimensions',
902 metavar='FOO bar',
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500903 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000904 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000905 '--optional-dimension',
906 default=[],
907 action='append',
908 nargs=3,
909 dest='optional_dimensions',
910 metavar='key value expiration',
Brad Hallf78187a2018-10-19 17:08:55 +0000911 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500912 parser.add_option_group(parser.filter_group)
913
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400914
Brad Hallf78187a2018-10-19 17:08:55 +0000915def _validate_filter_option(parser, key, value, expiration, argname):
916 if ':' in key:
917 parser.error('%s key cannot contain ":"' % argname)
918 if key.strip() != key:
919 parser.error('%s key has whitespace' % argname)
920 if not key:
921 parser.error('%s key is empty' % argname)
922
923 if value.strip() != value:
924 parser.error('%s value has whitespace' % argname)
925 if not value:
926 parser.error('%s value is empty' % argname)
927
928 if expiration is not None:
929 try:
930 expiration = int(expiration)
931 except ValueError:
932 parser.error('%s expiration is not an integer' % argname)
933 if expiration <= 0:
934 parser.error('%s expiration should be positive' % argname)
935 if expiration % 60 != 0:
936 parser.error('%s expiration is not divisible by 60' % argname)
937
938
maruelaf6b06c2017-06-08 06:26:53 -0700939def process_filter_options(parser, options):
940 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000941 _validate_filter_option(parser, key, value, None, 'dimension')
942 for key, value, exp in options.optional_dimensions:
943 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700944 options.dimensions.sort()
945
946
Vadim Shtayurab450c602014-05-12 19:23:25 -0700947def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400948 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700949 parser.sharding_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000950 '--shards',
951 type='int',
952 default=1,
953 metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700954 help='Number of shards to trigger and collect.')
955 parser.add_option_group(parser.sharding_group)
956
957
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400958def add_trigger_options(parser):
959 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500960 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400961 add_filter_options(parser)
962
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400963 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800964 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000965 '-s',
966 '--isolated',
967 metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500968 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800969 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000970 '-e',
971 '--env',
972 default=[],
973 action='append',
974 nargs=2,
975 metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700976 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800977 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000978 '--env-prefix',
979 default=[],
980 action='append',
981 nargs=2,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800982 metavar='VAR local/path',
983 help='Prepend task-relative `local/path` to the task\'s VAR environment '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000984 'variable using os-appropriate pathsep character. Can be specified '
985 'multiple times for the same VAR to add multiple paths.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800986 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000987 '--idempotent',
988 action='store_true',
989 default=False,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400990 help='When set, the server will actively try to find a previous task '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000991 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800992 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000993 '--secret-bytes-path',
994 metavar='FILE',
Stephen Martinisf391c772019-02-01 01:22:12 +0000995 help='The optional path to a file containing the secret_bytes to use '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000996 'with this task.')
maruel681d6802017-01-17 16:56:03 -0800997 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000998 '--hard-timeout',
999 type='int',
1000 default=60 * 60,
1001 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001002 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -08001003 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001004 '--io-timeout',
1005 type='int',
1006 default=20 * 60,
1007 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001008 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001009 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001010 '--lower-priority',
1011 action='store_true',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001012 help='Lowers the child process priority')
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001013 containment_choices = ('NONE', 'AUTO', 'JOB_OBJECT')
1014 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001015 '--containment-type',
1016 default='NONE',
1017 metavar='NONE',
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001018 choices=containment_choices,
1019 help='Containment to use; one of: %s' % ', '.join(containment_choices))
maruel681d6802017-01-17 16:56:03 -08001020 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001021 '--raw-cmd',
1022 action='store_true',
1023 default=False,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001024 help='When set, the command after -- is used as-is without run_isolated. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001025 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -08001026 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001027 '--relative-cwd',
1028 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001029 'requires --raw-cmd')
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001030 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001031 '--cipd-package',
1032 action='append',
1033 default=[],
1034 metavar='PKG',
maruel5475ba62017-05-31 15:35:47 -07001035 help='CIPD packages to install on the Swarming bot. Uses the format: '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001036 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001037 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001038 '--named-cache',
1039 action='append',
1040 nargs=2,
1041 default=[],
maruel5475ba62017-05-31 15:35:47 -07001042 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001043 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1044 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001045 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001046 help='Email of a service account to run the task as, or literal "bot" '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001047 'string to indicate that the task should use the same account the '
1048 'bot itself is using to authenticate to Swarming. Don\'t use task '
1049 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001050 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001051 '--pool-task-template',
1052 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1053 default='AUTO',
1054 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001055 'By default, the pool\'s TaskTemplate is automatically selected, '
1056 'according the pool configuration on the server. Choices are: '
1057 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
Robert Iannuccifafa7352018-06-13 17:08:17 +00001058 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001059 '-o',
1060 '--output',
1061 action='append',
1062 default=[],
1063 metavar='PATH',
maruel5475ba62017-05-31 15:35:47 -07001064 help='A list of files to return in addition to those written to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001065 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1066 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001067 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001068 '--wait-for-capacity',
1069 action='store_true',
1070 default=False,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001071 help='Instructs to leave the task PENDING even if there\'s no known bot '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001072 'that could run this task, otherwise the task will be denied with '
1073 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001074 parser.add_option_group(group)
1075
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001076 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001077 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001078 '--priority',
1079 type='int',
1080 default=200,
maruel681d6802017-01-17 16:56:03 -08001081 help='The lower value, the more important the task is')
1082 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001083 '-T',
1084 '--task-name',
1085 metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001086 help='Display name of the task. Defaults to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001087 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1088 'isolated file is provided, if a hash is provided, it defaults to '
1089 '<user>/<dimensions>/<isolated hash>/<timestamp>')
maruel681d6802017-01-17 16:56:03 -08001090 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001091 '--tags',
1092 action='append',
1093 default=[],
1094 metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001095 help='Tags to assign to the task.')
1096 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001097 '--user',
1098 default='',
maruel681d6802017-01-17 16:56:03 -08001099 help='User associated with the task. Defaults to authenticated user on '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001100 'the server.')
maruel681d6802017-01-17 16:56:03 -08001101 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001102 '--expiration',
1103 type='int',
1104 default=6 * 60 * 60,
1105 metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001106 help='Seconds to allow the task to be pending for a bot to run before '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001107 'this task request expires.')
maruel681d6802017-01-17 16:56:03 -08001108 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001109 '--deadline', type='int', dest='expiration', help=optparse.SUPPRESS_HELP)
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001110 group.add_option(
1111 '--realm',
1112 dest='realm',
1113 metavar='REALM',
1114 help='Realm associated with the task.')
Scott Lee44c13d72020-09-14 06:09:50 +00001115 group.add_option(
1116 '--resultdb',
1117 action='store_true',
1118 default=False,
1119 help='When set, the task is created with ResultDB enabled.')
maruel681d6802017-01-17 16:56:03 -08001120 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001121
1122
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001123def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001124 """Processes trigger options and does preparatory steps.
1125
1126 Returns:
1127 NewTaskRequest instance.
1128 """
maruelaf6b06c2017-06-08 06:26:53 -07001129 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001130 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001131 if args and args[0] == '--':
1132 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001133
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001134 if not options.dimensions:
1135 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001136 if not any(k == 'pool' for k, _v in options.dimensions):
1137 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001138 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1139 parser.error('--tags must be in the format key:value')
1140 if options.raw_cmd and not args:
1141 parser.error(
1142 'Arguments with --raw-cmd should be passed after -- as command '
1143 'delimiter.')
1144 if options.isolate_server and not options.namespace:
1145 parser.error(
1146 '--namespace must be a valid value when --isolate-server is used')
1147 if not options.isolated and not options.raw_cmd:
1148 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1149
1150 # Isolated
1151 # --isolated is required only if --raw-cmd wasn't provided.
1152 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1153 # preferred server.
Takuto Ikutaae767b32020-05-11 01:22:19 +00001154 isolateserver.process_isolate_server_options(parser, options,
1155 not options.raw_cmd)
maruel0a25f6c2017-05-10 10:43:23 -07001156 inputs_ref = None
1157 if options.isolate_server:
1158 inputs_ref = FilesRef(
1159 isolated=options.isolated,
1160 isolatedserver=options.isolate_server,
1161 namespace=options.namespace)
1162
1163 # Command
1164 command = None
1165 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001166 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001167 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001168 if options.relative_cwd:
1169 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1170 if not a.startswith(os.getcwd()):
1171 parser.error(
1172 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001173 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001174 if options.relative_cwd:
1175 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001176 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001177
maruel0a25f6c2017-05-10 10:43:23 -07001178 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001179 cipd_packages = []
1180 for p in options.cipd_package:
1181 split = p.split(':', 2)
1182 if len(split) != 3:
1183 parser.error('CIPD packages must take the form: path:package:version')
Junji Watanabe38b28b02020-04-23 10:23:30 +00001184 cipd_packages.append(
1185 CipdPackage(package_name=split[1], path=split[0], version=split[2]))
borenet02f772b2016-06-22 12:42:19 -07001186 cipd_input = None
1187 if cipd_packages:
1188 cipd_input = CipdInput(
Junji Watanabecb054042020-07-21 08:43:26 +00001189 client_package=None, packages=cipd_packages, server=None)
borenet02f772b2016-06-22 12:42:19 -07001190
maruel0a25f6c2017-05-10 10:43:23 -07001191 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001192 secret_bytes = None
1193 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001194 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001195 secret_bytes = f.read().encode('base64')
1196
maruel0a25f6c2017-05-10 10:43:23 -07001197 # Named caches
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001198 caches = [{
1199 u'name': six.text_type(i[0]),
1200 u'path': six.text_type(i[1])
1201 } for i in options.named_cache]
maruel0a25f6c2017-05-10 10:43:23 -07001202
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001203 env_prefixes = {}
1204 for k, v in options.env_prefix:
1205 env_prefixes.setdefault(k, []).append(v)
1206
Brad Hallf78187a2018-10-19 17:08:55 +00001207 # Get dimensions into the key/value format we can manipulate later.
Junji Watanabecb054042020-07-21 08:43:26 +00001208 orig_dims = [{
1209 'key': key,
1210 'value': value
1211 } for key, value in options.dimensions]
Brad Hallf78187a2018-10-19 17:08:55 +00001212 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1213
1214 # Construct base properties that we will use for all the slices, adding in
1215 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001216 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001217 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001218 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001219 command=command,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001220 containment=Containment(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001221 lower_priority=bool(options.lower_priority),
1222 containment_type=options.containment_type,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001223 ),
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001224 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001225 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001226 env=options.env,
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001227 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.items()],
maruel77f720b2015-09-15 12:35:22 -07001228 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001229 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001230 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001231 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001232 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001233 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001234 outputs=options.output,
1235 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001236
1237 slices = []
1238
1239 # Group the optional dimensions by expiration.
1240 dims_by_exp = {}
1241 for key, value, exp_secs in options.optional_dimensions:
Junji Watanabecb054042020-07-21 08:43:26 +00001242 dims_by_exp.setdefault(int(exp_secs), []).append({
1243 'key': key,
1244 'value': value
1245 })
Brad Hallf78187a2018-10-19 17:08:55 +00001246
1247 # Create the optional slices with expiration deltas, we fix up the properties
1248 # below.
1249 last_exp = 0
1250 for expiration_secs in sorted(dims_by_exp):
1251 t = TaskSlice(
1252 expiration_secs=expiration_secs - last_exp,
1253 properties=properties,
1254 wait_for_capacity=False)
1255 slices.append(t)
1256 last_exp = expiration_secs
1257
1258 # Add back in the default slice (the last one).
1259 exp = max(int(options.expiration) - last_exp, 60)
1260 base_task_slice = TaskSlice(
1261 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001262 properties=properties,
1263 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001264 slices.append(base_task_slice)
1265
Brad Hall7f463e62018-11-16 16:13:30 +00001266 # Add optional dimensions to the task slices, replacing a dimension that
1267 # has the same key if it is a dimension where repeating isn't valid (otherwise
1268 # we append it). Currently the only dimension we can repeat is "caches"; the
1269 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001270 extra_dims = []
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001271 for i, (_, kvs) in enumerate(sorted(dims_by_exp.items(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001272 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001273 # Replace or append the key/value pairs for this expiration in extra_dims;
1274 # we keep extra_dims around because we are iterating backwards and filling
1275 # in slices with shorter expirations. Dimensions expire as time goes on so
1276 # the slices that expire earlier will generally have more dimensions.
1277 for kv in kvs:
1278 if kv['key'] == 'caches':
1279 extra_dims.append(kv)
1280 else:
1281 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1282 # Then, add all the optional dimensions to the original dimension set, again
1283 # replacing if needed.
1284 for kv in extra_dims:
1285 if kv['key'] == 'caches':
1286 dims.append(kv)
1287 else:
1288 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001289 dims.sort(key=lambda x: (x['key'], x['value']))
1290 slice_properties = properties._replace(dimensions=dims)
1291 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1292
maruel77f720b2015-09-15 12:35:22 -07001293 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001294 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001295 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001296 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001297 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001298 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001299 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001300 user=options.user,
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001301 pool_task_template=options.pool_task_template,
Scott Lee44c13d72020-09-14 06:09:50 +00001302 realm=options.realm,
1303 resultdb={'enable': options.resultdb})
maruel@chromium.org0437a732013-08-27 16:05:52 +00001304
1305
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001306class TaskOutputStdoutOption(optparse.Option):
1307 """Where to output the each task's console output (stderr/stdout).
1308
1309 The output will be;
1310 none - not be downloaded.
1311 json - stored in summary.json file *only*.
1312 console - shown on stdout *only*.
1313 all - stored in summary.json and shown on stdout.
1314 """
1315
1316 choices = ['all', 'json', 'console', 'none']
1317
1318 def __init__(self, *args, **kw):
1319 optparse.Option.__init__(
1320 self,
1321 *args,
1322 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001323 default=['console', 'json'],
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001324 help=re.sub(r'\s\s*', ' ', self.__doc__),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001325 **kw)
1326
1327 def convert_value(self, opt, value):
1328 if value not in self.choices:
Junji Watanabecb054042020-07-21 08:43:26 +00001329 raise optparse.OptionValueError(
1330 "%s must be one of %s not %r" %
1331 (self.get_opt_string(), self.choices, value))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001332 stdout_to = []
1333 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001334 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001335 elif value != 'none':
1336 stdout_to = [value]
1337 return stdout_to
1338
1339
maruel@chromium.org0437a732013-08-27 16:05:52 +00001340def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001341 parser.server_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001342 '-t',
1343 '--timeout',
1344 type='float',
1345 default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001346 help='Timeout to wait for result, set to -1 for no timeout and get '
Junji Watanabecb054042020-07-21 08:43:26 +00001347 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001348 parser.group_logging.add_option(
1349 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001350 parser.group_logging.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001351 '--print-status-updates',
1352 action='store_true',
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001353 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001354 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001355 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001356 '--task-summary-json',
1357 metavar='FILE',
1358 help='Dump a summary of task results to this file as json. It contains '
Junji Watanabecb054042020-07-21 08:43:26 +00001359 'only shards statuses as know to server directly. Any output files '
1360 'emitted by the task can be collected by using --task-output-dir')
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001361 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001362 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001363 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001364 help='Directory to put task results into. When the task finishes, this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001365 'directory contains per-shard directory with output files produced '
1366 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001367 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001368 TaskOutputStdoutOption('--task-output-stdout'))
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001369 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001370 '--filepath-filter',
1371 help='This is regexp filter used to specify downloaded filepath when '
1372 'collecting isolated output.')
1373 parser.task_output_group.add_option(
1374 '--perf',
1375 action='store_true',
1376 default=False,
maruel9531ce02016-04-13 06:11:23 -07001377 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001378 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001379
1380
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001381def process_collect_options(parser, options):
1382 # Only negative -1 is allowed, disallow other negative values.
1383 if options.timeout != -1 and options.timeout < 0:
1384 parser.error('Invalid --timeout value')
1385
1386
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001387@subcommand.usage('bots...')
1388def CMDbot_delete(parser, args):
1389 """Forcibly deletes bots from the Swarming server."""
1390 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001391 '-f',
1392 '--force',
1393 action='store_true',
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001394 help='Do not prompt for confirmation')
1395 options, args = parser.parse_args(args)
1396 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001397 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001398
1399 bots = sorted(args)
1400 if not options.force:
1401 print('Delete the following bots?')
1402 for bot in bots:
1403 print(' %s' % bot)
1404 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1405 print('Goodbye.')
1406 return 1
1407
1408 result = 0
1409 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001410 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001411 if net.url_read_json(url, data={}, method='POST') is None:
1412 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001413 result = 1
1414 return result
1415
1416
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001417def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001418 """Returns information about the bots connected to the Swarming server."""
1419 add_filter_options(parser)
1420 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001421 '--dead-only',
1422 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001423 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001424 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001425 '-k',
1426 '--keep-dead',
1427 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001428 help='Keep both dead and alive bots')
1429 parser.filter_group.add_option(
1430 '--busy', action='store_true', help='Keep only busy bots')
1431 parser.filter_group.add_option(
1432 '--idle', action='store_true', help='Keep only idle bots')
1433 parser.filter_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001434 '--mp',
1435 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001436 help='Keep only Machine Provider managed bots')
1437 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001438 '--non-mp',
1439 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001440 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001441 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001442 '-b', '--bare', action='store_true', help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001443 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001444 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001445
1446 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001447 parser.error('Use only one of --keep-dead or --dead-only')
1448 if options.busy and options.idle:
1449 parser.error('Use only one of --busy or --idle')
1450 if options.mp and options.non_mp:
1451 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001452
smut281c3902018-05-30 17:50:05 -07001453 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001454 values = []
1455 if options.dead_only:
1456 values.append(('is_dead', 'TRUE'))
1457 elif options.keep_dead:
1458 values.append(('is_dead', 'NONE'))
1459 else:
1460 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001461
maruelaf6b06c2017-06-08 06:26:53 -07001462 if options.busy:
1463 values.append(('is_busy', 'TRUE'))
1464 elif options.idle:
1465 values.append(('is_busy', 'FALSE'))
1466 else:
1467 values.append(('is_busy', 'NONE'))
1468
1469 if options.mp:
1470 values.append(('is_mp', 'TRUE'))
1471 elif options.non_mp:
1472 values.append(('is_mp', 'FALSE'))
1473 else:
1474 values.append(('is_mp', 'NONE'))
1475
1476 for key, value in options.dimensions:
1477 values.append(('dimensions', '%s:%s' % (key, value)))
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +00001478 url += urllib.parse.urlencode(values)
maruelaf6b06c2017-06-08 06:26:53 -07001479 try:
1480 data, yielder = get_yielder(url, 0)
1481 bots = data.get('items') or []
1482 for items in yielder():
1483 if items:
1484 bots.extend(items)
1485 except Failure as e:
1486 sys.stderr.write('\n%s\n' % e)
1487 return 1
maruel77f720b2015-09-15 12:35:22 -07001488 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Lei Leife202df2019-06-11 17:33:34 +00001489 print(bot['bot_id'])
maruelaf6b06c2017-06-08 06:26:53 -07001490 if not options.bare:
1491 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Lei Leife202df2019-06-11 17:33:34 +00001492 print(' %s' % json.dumps(dimensions, sort_keys=True))
maruelaf6b06c2017-06-08 06:26:53 -07001493 if bot.get('task_id'):
Lei Leife202df2019-06-11 17:33:34 +00001494 print(' task: %s' % bot['task_id'])
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001495 return 0
1496
1497
maruelfd0a90c2016-06-10 11:51:10 -07001498@subcommand.usage('task_id')
1499def CMDcancel(parser, args):
1500 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001501 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001502 '-k',
1503 '--kill-running',
1504 action='store_true',
1505 default=False,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001506 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001507 options, args = parser.parse_args(args)
1508 if not args:
1509 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001510 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001511 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001512 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001513 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001514 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001515 print('Deleting %s failed. Probably already gone' % task_id)
1516 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001517 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001518 return 0
1519
1520
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001521@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001522def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001523 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001524
1525 The result can be in multiple part if the execution was sharded. It can
1526 potentially have retries.
1527 """
1528 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001529 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001530 '-j',
1531 '--json',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001532 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001533 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001534 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001535 if not args and not options.json:
1536 parser.error('Must specify at least one task id or --json.')
1537 if args and options.json:
1538 parser.error('Only use one of task id or --json.')
1539
1540 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001541 options.json = six.text_type(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001542 try:
maruel1ceb3872015-10-14 06:10:44 -07001543 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001544 data = json.load(f)
1545 except (IOError, ValueError):
1546 parser.error('Failed to open %s' % options.json)
1547 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001548 tasks = sorted(data['tasks'].values(), key=lambda x: x['shard_index'])
maruel71c61c82016-02-22 06:52:05 -08001549 args = [t['task_id'] for t in tasks]
1550 except (KeyError, TypeError):
1551 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001552 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001553 # Take in account all the task slices.
1554 offset = 0
1555 for s in data['request']['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +00001556 m = (
1557 offset + s['properties']['execution_timeout_secs'] +
1558 s['expiration_secs'])
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001559 if m > options.timeout:
1560 options.timeout = m
1561 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001562 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001563 else:
1564 valid = frozenset('0123456789abcdef')
1565 if any(not valid.issuperset(task_id) for task_id in args):
1566 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001567
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001568 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001569 return collect(options.swarming, args, options.timeout, options.decorate,
1570 options.print_status_updates, options.task_summary_json,
1571 options.task_output_dir, options.task_output_stdout,
1572 options.perf, options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001573 except Failure:
1574 on_error.report(None)
1575 return 1
1576
1577
maruel77f720b2015-09-15 12:35:22 -07001578@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001579def CMDpost(parser, args):
1580 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1581
1582 Input data must be sent to stdin, result is printed to stdout.
1583
1584 If HTTP response code >= 400, returns non-zero.
1585 """
1586 options, args = parser.parse_args(args)
1587 if len(args) != 1:
1588 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001589 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001590 data = sys.stdin.read()
1591 try:
1592 resp = net.url_read(url, data=data, method='POST')
1593 except net.TimeoutError:
1594 sys.stderr.write('Timeout!\n')
1595 return 1
1596 if not resp:
1597 sys.stderr.write('No response!\n')
1598 return 1
1599 sys.stdout.write(resp)
1600 return 0
1601
1602
1603@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001604def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001605 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1606 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001607
1608 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001609 Raw task request and results:
1610 swarming.py query -S server-url.com task/123456/request
1611 swarming.py query -S server-url.com task/123456/result
1612
maruel77f720b2015-09-15 12:35:22 -07001613 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001614 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001615
maruelaf6b06c2017-06-08 06:26:53 -07001616 Listing last 10 tasks on a specific bot named 'bot1':
1617 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001618
maruelaf6b06c2017-06-08 06:26:53 -07001619 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001620 quoting is important!:
1621 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001622 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001623 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001624 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001625 '-L',
1626 '--limit',
1627 type='int',
1628 default=200,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001629 help='Limit to enforce on limitless items (like number of tasks); '
Junji Watanabecb054042020-07-21 08:43:26 +00001630 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001631 parser.add_option(
1632 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001633 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001634 '--progress',
1635 action='store_true',
maruel77f720b2015-09-15 12:35:22 -07001636 help='Prints a dot at each request to show progress')
1637 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001638 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001639 parser.error(
1640 'Must specify only method name and optionally query args properly '
1641 'escaped.')
smut281c3902018-05-30 17:50:05 -07001642 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001643 try:
1644 data, yielder = get_yielder(base_url, options.limit)
1645 for items in yielder():
1646 if items:
1647 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001648 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001649 sys.stderr.write('.')
1650 sys.stderr.flush()
1651 except Failure as e:
1652 sys.stderr.write('\n%s\n' % e)
1653 return 1
maruel77f720b2015-09-15 12:35:22 -07001654 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001655 sys.stderr.write('\n')
1656 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001657 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001658 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001659 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001660 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001661 try:
maruel77f720b2015-09-15 12:35:22 -07001662 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001663 sys.stdout.write('\n')
1664 except IOError:
1665 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001666 return 0
1667
1668
maruel77f720b2015-09-15 12:35:22 -07001669def CMDquery_list(parser, args):
1670 """Returns list of all the Swarming APIs that can be used with command
1671 'query'.
1672 """
1673 parser.add_option(
1674 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1675 options, args = parser.parse_args(args)
1676 if args:
1677 parser.error('No argument allowed.')
1678
1679 try:
1680 apis = endpoints_api_discovery_apis(options.swarming)
1681 except APIError as e:
1682 parser.error(str(e))
1683 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001684 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001685 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001686 json.dump(apis, f)
1687 else:
1688 help_url = (
Junji Watanabecb054042020-07-21 08:43:26 +00001689 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1690 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001691 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -08001692 if i:
1693 print('')
Lei Leife202df2019-06-11 17:33:34 +00001694 print(api_id)
1695 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -08001696 if 'resources' in api:
1697 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001698 # TODO(maruel): Remove.
1699 # pylint: disable=too-many-nested-blocks
Junji Watanabecb054042020-07-21 08:43:26 +00001700 for j, (resource_name,
1701 resource) in enumerate(sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -08001702 if j:
1703 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001704 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -08001705 # Only list the GET ones.
1706 if method['httpMethod'] != 'GET':
1707 continue
Junji Watanabecb054042020-07-21 08:43:26 +00001708 print('- %s.%s: %s' % (resource_name, method_name, method['path']))
1709 print('\n'.join(' ' + l for l in textwrap.wrap(
1710 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +00001711 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -08001712 else:
1713 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001714 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -07001715 # Only list the GET ones.
1716 if method['httpMethod'] != 'GET':
1717 continue
Lei Leife202df2019-06-11 17:33:34 +00001718 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -08001719 print('\n'.join(
1720 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +00001721 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -07001722 return 0
1723
1724
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001725@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001726def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001727 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001728
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001729 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001730 """
1731 add_trigger_options(parser)
1732 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001733 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001734 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001735 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001736 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001737 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001738 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001739 except Failure as e:
Junji Watanabecb054042020-07-21 08:43:26 +00001740 on_error.report('Failed to trigger %s(%s): %s' %
1741 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001742 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001743 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001744 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001745 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001746 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001747 task_ids = [
Junji Watanabe38b28b02020-04-23 10:23:30 +00001748 t['task_id']
1749 for t in sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001750 ]
Caleb Rouleau779c4f02019-05-22 21:18:49 +00001751 for task_id in task_ids:
1752 print('Task: {server}/task?id={task}'.format(
1753 server=options.swarming, task=task_id))
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001754 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001755 offset = 0
1756 for s in task_request.task_slices:
Junji Watanabecb054042020-07-21 08:43:26 +00001757 m = (offset + s.properties.execution_timeout_secs + s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001758 if m > options.timeout:
1759 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001760 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001761 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001762 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001763 return collect(options.swarming, task_ids, options.timeout,
1764 options.decorate, options.print_status_updates,
1765 options.task_summary_json, options.task_output_dir,
1766 options.task_output_stdout, options.perf,
1767 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001768 except Failure:
1769 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001770 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001771
1772
maruel18122c62015-10-23 06:31:23 -07001773@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001774def CMDreproduce(parser, args):
1775 """Runs a task locally that was triggered on the server.
1776
1777 This running locally the same commands that have been run on the bot. The data
1778 downloaded will be in a subdirectory named 'work' of the current working
1779 directory.
maruel18122c62015-10-23 06:31:23 -07001780
1781 You can pass further additional arguments to the target command by passing
1782 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001783 """
maruelc070e672016-02-22 17:32:57 -08001784 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001785 '--output',
1786 metavar='DIR',
1787 default='out',
maruelc070e672016-02-22 17:32:57 -08001788 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001789 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001790 '--work',
1791 metavar='DIR',
1792 default='work',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001793 help='Directory to map the task input files into')
1794 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001795 '--cache',
1796 metavar='DIR',
1797 default='cache',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001798 help='Directory that contains the input cache')
1799 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001800 '--leak',
1801 action='store_true',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001802 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001803 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001804 extra_args = []
1805 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001806 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001807 if len(args) > 1:
1808 if args[1] == '--':
1809 if len(args) > 2:
1810 extra_args = args[2:]
1811 else:
1812 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001813
smut281c3902018-05-30 17:50:05 -07001814 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001815 request = net.url_read_json(url)
1816 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001817 print('Failed to retrieve request data for the task', file=sys.stderr)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001818 return 1
1819
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001820 workdir = six.text_type(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001821 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001822 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001823 fs.mkdir(workdir)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001824 cachedir = six.text_type(os.path.abspath('cipd_cache'))
iannucci31ab9192017-05-02 19:11:56 -07001825 if not fs.exists(cachedir):
1826 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001827
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001828 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001829 env = os.environ.copy()
1830 env['SWARMING_BOT_ID'] = 'reproduce'
1831 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001832 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001833 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001834 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001835 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001836 if not i['value']:
1837 env.pop(key, None)
1838 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001839 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001840
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001841 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001842 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001843 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001844 for i in env_prefixes:
1845 key = i['key']
1846 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001847 cur = env.get(key)
1848 if cur:
1849 paths.append(cur)
1850 env[key] = os.path.pathsep.join(paths)
1851
iannucci31ab9192017-05-02 19:11:56 -07001852 command = []
nodir152cba62016-05-12 16:08:56 -07001853 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001854 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001855 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +00001856 properties['inputs_ref']['isolatedserver'],
1857 properties['inputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001858 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001859 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1860 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1861 # leak.
1862 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001863 cache = local_caching.DiskContentAddressedCache(
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001864 six.text_type(os.path.abspath(options.cache)), policies, False)
maruel29ab2fd2015-10-16 11:44:01 -07001865 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001866 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001867 command = bundle.command
1868 if bundle.relative_cwd:
1869 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001870 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001871
1872 if properties.get('command'):
1873 command.extend(properties['command'])
1874
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001875 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Brian Sheedy7a761172019-08-30 22:55:14 +00001876 command = tools.find_executable(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001877 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001878 new_command = run_isolated.process_command(command, 'invalid', None)
1879 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001880 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001881 else:
1882 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001883 options.output = os.path.abspath(options.output)
Junji Watanabecb054042020-07-21 08:43:26 +00001884 new_command = run_isolated.process_command(command, options.output, None)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001885 if not os.path.isdir(options.output):
1886 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001887 command = new_command
1888 file_path.ensure_command_has_abs_path(command, workdir)
1889
1890 if properties.get('cipd_input'):
1891 ci = properties['cipd_input']
1892 cp = ci['client_package']
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001893 client_manager = cipd.get_client(cachedir, ci['server'], cp['package_name'],
1894 cp['version'])
iannucci31ab9192017-05-02 19:11:56 -07001895
1896 with client_manager as client:
1897 by_path = collections.defaultdict(list)
1898 for pkg in ci['packages']:
1899 path = pkg['path']
1900 # cipd deals with 'root' as ''
1901 if path == '.':
1902 path = ''
1903 by_path[path].append((pkg['package_name'], pkg['version']))
1904 client.ensure(workdir, by_path, cache_dir=cachedir)
1905
maruel77f720b2015-09-15 12:35:22 -07001906 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001907 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001908 except OSError as e:
Lei Leife202df2019-06-11 17:33:34 +00001909 print('Failed to run: %s' % ' '.join(command), file=sys.stderr)
1910 print(str(e), file=sys.stderr)
maruel77f720b2015-09-15 12:35:22 -07001911 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001912 finally:
1913 # Do not delete options.cache.
1914 if not options.leak:
1915 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001916
1917
maruel0eb1d1b2015-10-02 14:48:21 -07001918@subcommand.usage('bot_id')
1919def CMDterminate(parser, args):
1920 """Tells a bot to gracefully shut itself down as soon as it can.
1921
1922 This is done by completing whatever current task there is then exiting the bot
1923 process.
1924 """
1925 parser.add_option(
1926 '--wait', action='store_true', help='Wait for the bot to terminate')
1927 options, args = parser.parse_args(args)
1928 if len(args) != 1:
1929 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001930 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001931 request = net.url_read_json(url, data={})
1932 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001933 print('Failed to ask for termination', file=sys.stderr)
maruel0eb1d1b2015-10-02 14:48:21 -07001934 return 1
1935 if options.wait:
Junji Watanabecb054042020-07-21 08:43:26 +00001936 return collect(options.swarming, [request['task_id']], 0., False, False,
1937 None, None, [], False, None)
maruelbfc5f872017-06-10 16:43:17 -07001938 else:
Lei Leife202df2019-06-11 17:33:34 +00001939 print(request['task_id'])
maruel0eb1d1b2015-10-02 14:48:21 -07001940 return 0
1941
1942
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001943@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001944def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001945 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001946
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001947 Passes all extra arguments provided after '--' as additional command line
1948 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001949 """
1950 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001951 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001952 parser.add_option(
1953 '--dump-json',
1954 metavar='FILE',
1955 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001956 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001957 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001958 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001959 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001960 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001961 print('Triggered task: %s' % task_request.name)
Junji Watanabecb054042020-07-21 08:43:26 +00001962 tasks_sorted = sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001963 if options.dump_json:
1964 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001965 'base_task_name': task_request.name,
1966 'tasks': tasks,
1967 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001968 }
maruel46b015f2015-10-13 18:40:35 -07001969 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001970 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001971 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001972 (options.swarming, options.dump_json))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001973 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001974 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001975 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001976 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001977 print('Or visit:')
1978 for t in tasks_sorted:
1979 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001980 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001981 except Failure:
1982 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001983 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001984
1985
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001986class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +00001987
maruel@chromium.org0437a732013-08-27 16:05:52 +00001988 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001989 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001990 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001991 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001992 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001993 '-S',
1994 '--swarming',
1995 metavar='URL',
1996 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001997 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001998 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001999 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002000
2001 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04002002 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00002003 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002004 auth.process_auth_options(self, options)
2005 user = self._process_swarming(options)
2006 if hasattr(options, 'user') and not options.user:
2007 options.user = user
2008 return options, args
2009
2010 def _process_swarming(self, options):
2011 """Processes the --swarming option and aborts if not specified.
2012
2013 Returns the identity as determined by the server.
2014 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00002015 if not options.swarming:
2016 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002017 try:
2018 options.swarming = net.fix_url(options.swarming)
2019 except ValueError as e:
2020 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +00002021
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05002022 try:
2023 user = auth.ensure_logged_in(options.swarming)
2024 except ValueError as e:
2025 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002026 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00002027
2028
2029def main(args):
2030 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04002031 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002032
2033
2034if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07002035 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00002036 fix_encoding.fix_encoding()
2037 tools.disable_buffering()
2038 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +00002039 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002040 sys.exit(main(sys.argv[1:]))