blob: 9a65da3f90163b934ddac145ec6c8f9cc589dc05 [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 Lei73a5f732020-03-23 20:36:14 +0000773 # s is bytes in Python3, print could not print
774 # s with nice format, so decode s to str.
775 s = six.ensure_str(
776 decorate_shard_output(swarming, index, metadata,
777 "console" in task_output_stdout).encode(
778 'utf-8', 'replace'))
leileied181762016-10-13 14:24:59 -0700779 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400780 if len(seen_shards) < len(task_ids):
781 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782 else:
Junji Watanabecb054042020-07-21 08:43:26 +0000783 print('%s: %s %s' % (metadata.get(
784 'bot_id', 'N/A'), metadata['task_id'], shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000785 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700786 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400787 if output:
788 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700789 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700790 summary = output_collector.finalize()
791 if task_summary_json:
792 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700793
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400794 if decorate and total_duration:
795 print('Total duration: %.1fs' % total_duration)
796
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400797 if len(seen_shards) != len(task_ids):
798 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Junji Watanabecb054042020-07-21 08:43:26 +0000799 print(
800 'Results from some shards are missing: %s' %
801 ', '.join(map(str, missing_shards)),
802 file=sys.stderr)
Vadim Shtayurac524f512014-05-15 09:54:56 -0700803 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700804
maruela5490782015-09-30 10:56:59 -0700805 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000806
807
maruel77f720b2015-09-15 12:35:22 -0700808### API management.
809
810
811class APIError(Exception):
812 pass
813
814
815def endpoints_api_discovery_apis(host):
816 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
817 the APIs exposed by a host.
818
819 https://developers.google.com/discovery/v1/reference/apis/list
820 """
maruel380e3262016-08-31 16:10:06 -0700821 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
822 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700823 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
824 if data is None:
825 raise APIError('Failed to discover APIs on %s' % host)
826 out = {}
827 for api in data['items']:
828 if api['id'] == 'discovery:v1':
829 continue
830 # URL is of the following form:
831 # url = host + (
832 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
833 api_data = net.url_read_json(api['discoveryRestUrl'])
834 if api_data is None:
835 raise APIError('Failed to discover %s on %s' % (api['id'], host))
836 out[api['id']] = api_data
837 return out
838
839
maruelaf6b06c2017-06-08 06:26:53 -0700840def get_yielder(base_url, limit):
841 """Returns the first query and a function that yields following items."""
842 CHUNK_SIZE = 250
843
844 url = base_url
845 if limit:
846 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
847 data = net.url_read_json(url)
848 if data is None:
849 # TODO(maruel): Do basic diagnostic.
850 raise Failure('Failed to access %s' % url)
851 org_cursor = data.pop('cursor', None)
852 org_total = len(data.get('items') or [])
853 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
854 if not org_cursor or not org_total:
855 # This is not an iterable resource.
856 return data, lambda: []
857
858 def yielder():
859 cursor = org_cursor
860 total = org_total
861 # Some items support cursors. Try to get automatically if cursors are needed
862 # by looking at the 'cursor' items.
863 while cursor and (not limit or total < limit):
864 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000865 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -0700866 if limit:
867 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
868 new = net.url_read_json(url)
869 if new is None:
870 raise Failure('Failed to access %s' % url)
871 cursor = new.get('cursor')
872 new_items = new.get('items')
873 nb_items = len(new_items or [])
874 total += nb_items
875 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
876 yield new_items
877
878 return data, yielder
879
880
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500881### Commands.
882
883
884def abort_task(_swarming, _manifest):
885 """Given a task manifest that was triggered, aborts its execution."""
886 # TODO(vadimsh): No supported by the server yet.
887
888
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400889def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800890 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500891 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000892 '-d',
893 '--dimension',
894 default=[],
895 action='append',
896 nargs=2,
897 dest='dimensions',
898 metavar='FOO bar',
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500899 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000900 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000901 '--optional-dimension',
902 default=[],
903 action='append',
904 nargs=3,
905 dest='optional_dimensions',
906 metavar='key value expiration',
Brad Hallf78187a2018-10-19 17:08:55 +0000907 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500908 parser.add_option_group(parser.filter_group)
909
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400910
Brad Hallf78187a2018-10-19 17:08:55 +0000911def _validate_filter_option(parser, key, value, expiration, argname):
912 if ':' in key:
913 parser.error('%s key cannot contain ":"' % argname)
914 if key.strip() != key:
915 parser.error('%s key has whitespace' % argname)
916 if not key:
917 parser.error('%s key is empty' % argname)
918
919 if value.strip() != value:
920 parser.error('%s value has whitespace' % argname)
921 if not value:
922 parser.error('%s value is empty' % argname)
923
924 if expiration is not None:
925 try:
926 expiration = int(expiration)
927 except ValueError:
928 parser.error('%s expiration is not an integer' % argname)
929 if expiration <= 0:
930 parser.error('%s expiration should be positive' % argname)
931 if expiration % 60 != 0:
932 parser.error('%s expiration is not divisible by 60' % argname)
933
934
maruelaf6b06c2017-06-08 06:26:53 -0700935def process_filter_options(parser, options):
936 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000937 _validate_filter_option(parser, key, value, None, 'dimension')
938 for key, value, exp in options.optional_dimensions:
939 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700940 options.dimensions.sort()
941
942
Vadim Shtayurab450c602014-05-12 19:23:25 -0700943def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400944 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700945 parser.sharding_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000946 '--shards',
947 type='int',
948 default=1,
949 metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700950 help='Number of shards to trigger and collect.')
951 parser.add_option_group(parser.sharding_group)
952
953
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400954def add_trigger_options(parser):
955 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500956 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400957 add_filter_options(parser)
958
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400959 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800960 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000961 '-s',
962 '--isolated',
963 metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500964 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800965 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000966 '-e',
967 '--env',
968 default=[],
969 action='append',
970 nargs=2,
971 metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700972 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800973 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000974 '--env-prefix',
975 default=[],
976 action='append',
977 nargs=2,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800978 metavar='VAR local/path',
979 help='Prepend task-relative `local/path` to the task\'s VAR environment '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000980 'variable using os-appropriate pathsep character. Can be specified '
981 'multiple times for the same VAR to add multiple paths.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800982 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000983 '--idempotent',
984 action='store_true',
985 default=False,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400986 help='When set, the server will actively try to find a previous task '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000987 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800988 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000989 '--secret-bytes-path',
990 metavar='FILE',
Stephen Martinisf391c772019-02-01 01:22:12 +0000991 help='The optional path to a file containing the secret_bytes to use '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000992 'with this task.')
maruel681d6802017-01-17 16:56:03 -0800993 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000994 '--hard-timeout',
995 type='int',
996 default=60 * 60,
997 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400998 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800999 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001000 '--io-timeout',
1001 type='int',
1002 default=20 * 60,
1003 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001004 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001005 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001006 '--lower-priority',
1007 action='store_true',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001008 help='Lowers the child process priority')
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001009 containment_choices = ('NONE', 'AUTO', 'JOB_OBJECT')
1010 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001011 '--containment-type',
1012 default='NONE',
1013 metavar='NONE',
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001014 choices=containment_choices,
1015 help='Containment to use; one of: %s' % ', '.join(containment_choices))
maruel681d6802017-01-17 16:56:03 -08001016 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001017 '--raw-cmd',
1018 action='store_true',
1019 default=False,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001020 help='When set, the command after -- is used as-is without run_isolated. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001021 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -08001022 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001023 '--relative-cwd',
1024 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001025 'requires --raw-cmd')
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001026 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001027 '--cipd-package',
1028 action='append',
1029 default=[],
1030 metavar='PKG',
maruel5475ba62017-05-31 15:35:47 -07001031 help='CIPD packages to install on the Swarming bot. Uses the format: '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001032 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001033 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001034 '--named-cache',
1035 action='append',
1036 nargs=2,
1037 default=[],
maruel5475ba62017-05-31 15:35:47 -07001038 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001039 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1040 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001041 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001042 help='Email of a service account to run the task as, or literal "bot" '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001043 'string to indicate that the task should use the same account the '
1044 'bot itself is using to authenticate to Swarming. Don\'t use task '
1045 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001046 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001047 '--pool-task-template',
1048 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1049 default='AUTO',
1050 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001051 'By default, the pool\'s TaskTemplate is automatically selected, '
1052 'according the pool configuration on the server. Choices are: '
1053 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
Robert Iannuccifafa7352018-06-13 17:08:17 +00001054 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001055 '-o',
1056 '--output',
1057 action='append',
1058 default=[],
1059 metavar='PATH',
maruel5475ba62017-05-31 15:35:47 -07001060 help='A list of files to return in addition to those written to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001061 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1062 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001063 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001064 '--wait-for-capacity',
1065 action='store_true',
1066 default=False,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001067 help='Instructs to leave the task PENDING even if there\'s no known bot '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001068 'that could run this task, otherwise the task will be denied with '
1069 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001070 parser.add_option_group(group)
1071
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001072 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001073 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001074 '--priority',
1075 type='int',
1076 default=200,
maruel681d6802017-01-17 16:56:03 -08001077 help='The lower value, the more important the task is')
1078 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001079 '-T',
1080 '--task-name',
1081 metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001082 help='Display name of the task. Defaults to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001083 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1084 'isolated file is provided, if a hash is provided, it defaults to '
1085 '<user>/<dimensions>/<isolated hash>/<timestamp>')
maruel681d6802017-01-17 16:56:03 -08001086 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001087 '--tags',
1088 action='append',
1089 default=[],
1090 metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001091 help='Tags to assign to the task.')
1092 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001093 '--user',
1094 default='',
maruel681d6802017-01-17 16:56:03 -08001095 help='User associated with the task. Defaults to authenticated user on '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001096 'the server.')
maruel681d6802017-01-17 16:56:03 -08001097 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001098 '--expiration',
1099 type='int',
1100 default=6 * 60 * 60,
1101 metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001102 help='Seconds to allow the task to be pending for a bot to run before '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001103 'this task request expires.')
maruel681d6802017-01-17 16:56:03 -08001104 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001105 '--deadline', type='int', dest='expiration', help=optparse.SUPPRESS_HELP)
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001106 group.add_option(
1107 '--realm',
1108 dest='realm',
1109 metavar='REALM',
1110 help='Realm associated with the task.')
Scott Lee44c13d72020-09-14 06:09:50 +00001111 group.add_option(
1112 '--resultdb',
1113 action='store_true',
1114 default=False,
1115 help='When set, the task is created with ResultDB enabled.')
maruel681d6802017-01-17 16:56:03 -08001116 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001117
1118
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001119def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001120 """Processes trigger options and does preparatory steps.
1121
1122 Returns:
1123 NewTaskRequest instance.
1124 """
maruelaf6b06c2017-06-08 06:26:53 -07001125 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001126 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001127 if args and args[0] == '--':
1128 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001129
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001130 if not options.dimensions:
1131 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001132 if not any(k == 'pool' for k, _v in options.dimensions):
1133 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001134 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1135 parser.error('--tags must be in the format key:value')
1136 if options.raw_cmd and not args:
1137 parser.error(
1138 'Arguments with --raw-cmd should be passed after -- as command '
1139 'delimiter.')
1140 if options.isolate_server and not options.namespace:
1141 parser.error(
1142 '--namespace must be a valid value when --isolate-server is used')
1143 if not options.isolated and not options.raw_cmd:
1144 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1145
1146 # Isolated
1147 # --isolated is required only if --raw-cmd wasn't provided.
1148 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1149 # preferred server.
Takuto Ikutaae767b32020-05-11 01:22:19 +00001150 isolateserver.process_isolate_server_options(parser, options,
1151 not options.raw_cmd)
maruel0a25f6c2017-05-10 10:43:23 -07001152 inputs_ref = None
1153 if options.isolate_server:
1154 inputs_ref = FilesRef(
1155 isolated=options.isolated,
1156 isolatedserver=options.isolate_server,
1157 namespace=options.namespace)
1158
1159 # Command
1160 command = None
1161 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001162 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001163 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001164 if options.relative_cwd:
1165 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1166 if not a.startswith(os.getcwd()):
1167 parser.error(
1168 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001169 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001170 if options.relative_cwd:
1171 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001172 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001173
maruel0a25f6c2017-05-10 10:43:23 -07001174 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001175 cipd_packages = []
1176 for p in options.cipd_package:
1177 split = p.split(':', 2)
1178 if len(split) != 3:
1179 parser.error('CIPD packages must take the form: path:package:version')
Junji Watanabe38b28b02020-04-23 10:23:30 +00001180 cipd_packages.append(
1181 CipdPackage(package_name=split[1], path=split[0], version=split[2]))
borenet02f772b2016-06-22 12:42:19 -07001182 cipd_input = None
1183 if cipd_packages:
1184 cipd_input = CipdInput(
Junji Watanabecb054042020-07-21 08:43:26 +00001185 client_package=None, packages=cipd_packages, server=None)
borenet02f772b2016-06-22 12:42:19 -07001186
maruel0a25f6c2017-05-10 10:43:23 -07001187 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001188 secret_bytes = None
1189 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001190 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001191 secret_bytes = f.read().encode('base64')
1192
maruel0a25f6c2017-05-10 10:43:23 -07001193 # Named caches
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001194 caches = [{
1195 u'name': six.text_type(i[0]),
1196 u'path': six.text_type(i[1])
1197 } for i in options.named_cache]
maruel0a25f6c2017-05-10 10:43:23 -07001198
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001199 env_prefixes = {}
1200 for k, v in options.env_prefix:
1201 env_prefixes.setdefault(k, []).append(v)
1202
Brad Hallf78187a2018-10-19 17:08:55 +00001203 # Get dimensions into the key/value format we can manipulate later.
Junji Watanabecb054042020-07-21 08:43:26 +00001204 orig_dims = [{
1205 'key': key,
1206 'value': value
1207 } for key, value in options.dimensions]
Brad Hallf78187a2018-10-19 17:08:55 +00001208 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1209
1210 # Construct base properties that we will use for all the slices, adding in
1211 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001212 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001213 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001214 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001215 command=command,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001216 containment=Containment(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001217 lower_priority=bool(options.lower_priority),
1218 containment_type=options.containment_type,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001219 ),
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001220 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001221 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001222 env=options.env,
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001223 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.items()],
maruel77f720b2015-09-15 12:35:22 -07001224 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001225 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001226 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001227 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001228 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001229 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001230 outputs=options.output,
1231 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001232
1233 slices = []
1234
1235 # Group the optional dimensions by expiration.
1236 dims_by_exp = {}
1237 for key, value, exp_secs in options.optional_dimensions:
Junji Watanabecb054042020-07-21 08:43:26 +00001238 dims_by_exp.setdefault(int(exp_secs), []).append({
1239 'key': key,
1240 'value': value
1241 })
Brad Hallf78187a2018-10-19 17:08:55 +00001242
1243 # Create the optional slices with expiration deltas, we fix up the properties
1244 # below.
1245 last_exp = 0
1246 for expiration_secs in sorted(dims_by_exp):
1247 t = TaskSlice(
1248 expiration_secs=expiration_secs - last_exp,
1249 properties=properties,
1250 wait_for_capacity=False)
1251 slices.append(t)
1252 last_exp = expiration_secs
1253
1254 # Add back in the default slice (the last one).
1255 exp = max(int(options.expiration) - last_exp, 60)
1256 base_task_slice = TaskSlice(
1257 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001258 properties=properties,
1259 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001260 slices.append(base_task_slice)
1261
Brad Hall7f463e62018-11-16 16:13:30 +00001262 # Add optional dimensions to the task slices, replacing a dimension that
1263 # has the same key if it is a dimension where repeating isn't valid (otherwise
1264 # we append it). Currently the only dimension we can repeat is "caches"; the
1265 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001266 extra_dims = []
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001267 for i, (_, kvs) in enumerate(sorted(dims_by_exp.items(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001268 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001269 # Replace or append the key/value pairs for this expiration in extra_dims;
1270 # we keep extra_dims around because we are iterating backwards and filling
1271 # in slices with shorter expirations. Dimensions expire as time goes on so
1272 # the slices that expire earlier will generally have more dimensions.
1273 for kv in kvs:
1274 if kv['key'] == 'caches':
1275 extra_dims.append(kv)
1276 else:
1277 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1278 # Then, add all the optional dimensions to the original dimension set, again
1279 # replacing if needed.
1280 for kv in extra_dims:
1281 if kv['key'] == 'caches':
1282 dims.append(kv)
1283 else:
1284 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001285 dims.sort(key=lambda x: (x['key'], x['value']))
1286 slice_properties = properties._replace(dimensions=dims)
1287 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1288
maruel77f720b2015-09-15 12:35:22 -07001289 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001290 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001291 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001292 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001293 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001294 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001295 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001296 user=options.user,
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001297 pool_task_template=options.pool_task_template,
Scott Lee44c13d72020-09-14 06:09:50 +00001298 realm=options.realm,
1299 resultdb={'enable': options.resultdb})
maruel@chromium.org0437a732013-08-27 16:05:52 +00001300
1301
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001302class TaskOutputStdoutOption(optparse.Option):
1303 """Where to output the each task's console output (stderr/stdout).
1304
1305 The output will be;
1306 none - not be downloaded.
1307 json - stored in summary.json file *only*.
1308 console - shown on stdout *only*.
1309 all - stored in summary.json and shown on stdout.
1310 """
1311
1312 choices = ['all', 'json', 'console', 'none']
1313
1314 def __init__(self, *args, **kw):
1315 optparse.Option.__init__(
1316 self,
1317 *args,
1318 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001319 default=['console', 'json'],
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001320 help=re.sub(r'\s\s*', ' ', self.__doc__),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001321 **kw)
1322
1323 def convert_value(self, opt, value):
1324 if value not in self.choices:
Junji Watanabecb054042020-07-21 08:43:26 +00001325 raise optparse.OptionValueError(
1326 "%s must be one of %s not %r" %
1327 (self.get_opt_string(), self.choices, value))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001328 stdout_to = []
1329 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001330 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001331 elif value != 'none':
1332 stdout_to = [value]
1333 return stdout_to
1334
1335
maruel@chromium.org0437a732013-08-27 16:05:52 +00001336def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001337 parser.server_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001338 '-t',
1339 '--timeout',
1340 type='float',
1341 default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001342 help='Timeout to wait for result, set to -1 for no timeout and get '
Junji Watanabecb054042020-07-21 08:43:26 +00001343 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001344 parser.group_logging.add_option(
1345 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001346 parser.group_logging.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001347 '--print-status-updates',
1348 action='store_true',
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001349 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001350 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001351 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001352 '--task-summary-json',
1353 metavar='FILE',
1354 help='Dump a summary of task results to this file as json. It contains '
Junji Watanabecb054042020-07-21 08:43:26 +00001355 'only shards statuses as know to server directly. Any output files '
1356 'emitted by the task can be collected by using --task-output-dir')
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001357 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001358 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001359 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001360 help='Directory to put task results into. When the task finishes, this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001361 'directory contains per-shard directory with output files produced '
1362 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001363 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001364 TaskOutputStdoutOption('--task-output-stdout'))
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001365 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001366 '--filepath-filter',
1367 help='This is regexp filter used to specify downloaded filepath when '
1368 'collecting isolated output.')
1369 parser.task_output_group.add_option(
1370 '--perf',
1371 action='store_true',
1372 default=False,
maruel9531ce02016-04-13 06:11:23 -07001373 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001374 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001375
1376
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001377def process_collect_options(parser, options):
1378 # Only negative -1 is allowed, disallow other negative values.
1379 if options.timeout != -1 and options.timeout < 0:
1380 parser.error('Invalid --timeout value')
1381
1382
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001383@subcommand.usage('bots...')
1384def CMDbot_delete(parser, args):
1385 """Forcibly deletes bots from the Swarming server."""
1386 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001387 '-f',
1388 '--force',
1389 action='store_true',
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001390 help='Do not prompt for confirmation')
1391 options, args = parser.parse_args(args)
1392 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001393 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001394
1395 bots = sorted(args)
1396 if not options.force:
1397 print('Delete the following bots?')
1398 for bot in bots:
1399 print(' %s' % bot)
1400 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1401 print('Goodbye.')
1402 return 1
1403
1404 result = 0
1405 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001406 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001407 if net.url_read_json(url, data={}, method='POST') is None:
1408 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001409 result = 1
1410 return result
1411
1412
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001413def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001414 """Returns information about the bots connected to the Swarming server."""
1415 add_filter_options(parser)
1416 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001417 '--dead-only',
1418 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001419 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001420 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001421 '-k',
1422 '--keep-dead',
1423 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001424 help='Keep both dead and alive bots')
1425 parser.filter_group.add_option(
1426 '--busy', action='store_true', help='Keep only busy bots')
1427 parser.filter_group.add_option(
1428 '--idle', action='store_true', help='Keep only idle bots')
1429 parser.filter_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001430 '--mp',
1431 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001432 help='Keep only Machine Provider managed bots')
1433 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001434 '--non-mp',
1435 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001436 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001437 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001438 '-b', '--bare', action='store_true', help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001439 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001440 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001441
1442 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001443 parser.error('Use only one of --keep-dead or --dead-only')
1444 if options.busy and options.idle:
1445 parser.error('Use only one of --busy or --idle')
1446 if options.mp and options.non_mp:
1447 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001448
smut281c3902018-05-30 17:50:05 -07001449 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001450 values = []
1451 if options.dead_only:
1452 values.append(('is_dead', 'TRUE'))
1453 elif options.keep_dead:
1454 values.append(('is_dead', 'NONE'))
1455 else:
1456 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001457
maruelaf6b06c2017-06-08 06:26:53 -07001458 if options.busy:
1459 values.append(('is_busy', 'TRUE'))
1460 elif options.idle:
1461 values.append(('is_busy', 'FALSE'))
1462 else:
1463 values.append(('is_busy', 'NONE'))
1464
1465 if options.mp:
1466 values.append(('is_mp', 'TRUE'))
1467 elif options.non_mp:
1468 values.append(('is_mp', 'FALSE'))
1469 else:
1470 values.append(('is_mp', 'NONE'))
1471
1472 for key, value in options.dimensions:
1473 values.append(('dimensions', '%s:%s' % (key, value)))
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +00001474 url += urllib.parse.urlencode(values)
maruelaf6b06c2017-06-08 06:26:53 -07001475 try:
1476 data, yielder = get_yielder(url, 0)
1477 bots = data.get('items') or []
1478 for items in yielder():
1479 if items:
1480 bots.extend(items)
1481 except Failure as e:
1482 sys.stderr.write('\n%s\n' % e)
1483 return 1
maruel77f720b2015-09-15 12:35:22 -07001484 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Lei Leife202df2019-06-11 17:33:34 +00001485 print(bot['bot_id'])
maruelaf6b06c2017-06-08 06:26:53 -07001486 if not options.bare:
1487 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Lei Leife202df2019-06-11 17:33:34 +00001488 print(' %s' % json.dumps(dimensions, sort_keys=True))
maruelaf6b06c2017-06-08 06:26:53 -07001489 if bot.get('task_id'):
Lei Leife202df2019-06-11 17:33:34 +00001490 print(' task: %s' % bot['task_id'])
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001491 return 0
1492
1493
maruelfd0a90c2016-06-10 11:51:10 -07001494@subcommand.usage('task_id')
1495def CMDcancel(parser, args):
1496 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001497 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001498 '-k',
1499 '--kill-running',
1500 action='store_true',
1501 default=False,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001502 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001503 options, args = parser.parse_args(args)
1504 if not args:
1505 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001506 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001507 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001508 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001509 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001510 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001511 print('Deleting %s failed. Probably already gone' % task_id)
1512 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001513 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001514 return 0
1515
1516
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001517@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001518def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001519 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001520
1521 The result can be in multiple part if the execution was sharded. It can
1522 potentially have retries.
1523 """
1524 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001525 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001526 '-j',
1527 '--json',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001528 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001529 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001530 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001531 if not args and not options.json:
1532 parser.error('Must specify at least one task id or --json.')
1533 if args and options.json:
1534 parser.error('Only use one of task id or --json.')
1535
1536 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001537 options.json = six.text_type(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001538 try:
maruel1ceb3872015-10-14 06:10:44 -07001539 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001540 data = json.load(f)
1541 except (IOError, ValueError):
1542 parser.error('Failed to open %s' % options.json)
1543 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001544 tasks = sorted(data['tasks'].values(), key=lambda x: x['shard_index'])
maruel71c61c82016-02-22 06:52:05 -08001545 args = [t['task_id'] for t in tasks]
1546 except (KeyError, TypeError):
1547 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001548 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001549 # Take in account all the task slices.
1550 offset = 0
1551 for s in data['request']['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +00001552 m = (
1553 offset + s['properties']['execution_timeout_secs'] +
1554 s['expiration_secs'])
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001555 if m > options.timeout:
1556 options.timeout = m
1557 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001558 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001559 else:
1560 valid = frozenset('0123456789abcdef')
1561 if any(not valid.issuperset(task_id) for task_id in args):
1562 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001563
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001564 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001565 return collect(options.swarming, args, options.timeout, options.decorate,
1566 options.print_status_updates, options.task_summary_json,
1567 options.task_output_dir, options.task_output_stdout,
1568 options.perf, options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001569 except Failure:
1570 on_error.report(None)
1571 return 1
1572
1573
maruel77f720b2015-09-15 12:35:22 -07001574@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001575def CMDpost(parser, args):
1576 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1577
1578 Input data must be sent to stdin, result is printed to stdout.
1579
1580 If HTTP response code >= 400, returns non-zero.
1581 """
1582 options, args = parser.parse_args(args)
1583 if len(args) != 1:
1584 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001585 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001586 data = sys.stdin.read()
1587 try:
1588 resp = net.url_read(url, data=data, method='POST')
1589 except net.TimeoutError:
1590 sys.stderr.write('Timeout!\n')
1591 return 1
1592 if not resp:
1593 sys.stderr.write('No response!\n')
1594 return 1
1595 sys.stdout.write(resp)
1596 return 0
1597
1598
1599@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001600def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001601 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1602 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001603
1604 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001605 Raw task request and results:
1606 swarming.py query -S server-url.com task/123456/request
1607 swarming.py query -S server-url.com task/123456/result
1608
maruel77f720b2015-09-15 12:35:22 -07001609 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001610 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001611
maruelaf6b06c2017-06-08 06:26:53 -07001612 Listing last 10 tasks on a specific bot named 'bot1':
1613 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001614
maruelaf6b06c2017-06-08 06:26:53 -07001615 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001616 quoting is important!:
1617 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001618 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001619 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001620 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001621 '-L',
1622 '--limit',
1623 type='int',
1624 default=200,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001625 help='Limit to enforce on limitless items (like number of tasks); '
Junji Watanabecb054042020-07-21 08:43:26 +00001626 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001627 parser.add_option(
1628 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001629 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001630 '--progress',
1631 action='store_true',
maruel77f720b2015-09-15 12:35:22 -07001632 help='Prints a dot at each request to show progress')
1633 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001634 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001635 parser.error(
1636 'Must specify only method name and optionally query args properly '
1637 'escaped.')
smut281c3902018-05-30 17:50:05 -07001638 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001639 try:
1640 data, yielder = get_yielder(base_url, options.limit)
1641 for items in yielder():
1642 if items:
1643 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001644 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001645 sys.stderr.write('.')
1646 sys.stderr.flush()
1647 except Failure as e:
1648 sys.stderr.write('\n%s\n' % e)
1649 return 1
maruel77f720b2015-09-15 12:35:22 -07001650 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001651 sys.stderr.write('\n')
1652 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001653 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001654 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001655 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001656 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001657 try:
maruel77f720b2015-09-15 12:35:22 -07001658 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001659 sys.stdout.write('\n')
1660 except IOError:
1661 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001662 return 0
1663
1664
maruel77f720b2015-09-15 12:35:22 -07001665def CMDquery_list(parser, args):
1666 """Returns list of all the Swarming APIs that can be used with command
1667 'query'.
1668 """
1669 parser.add_option(
1670 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1671 options, args = parser.parse_args(args)
1672 if args:
1673 parser.error('No argument allowed.')
1674
1675 try:
1676 apis = endpoints_api_discovery_apis(options.swarming)
1677 except APIError as e:
1678 parser.error(str(e))
1679 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001680 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001681 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001682 json.dump(apis, f)
1683 else:
1684 help_url = (
Junji Watanabecb054042020-07-21 08:43:26 +00001685 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1686 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001687 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -08001688 if i:
1689 print('')
Lei Leife202df2019-06-11 17:33:34 +00001690 print(api_id)
1691 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -08001692 if 'resources' in api:
1693 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001694 # TODO(maruel): Remove.
1695 # pylint: disable=too-many-nested-blocks
Junji Watanabecb054042020-07-21 08:43:26 +00001696 for j, (resource_name,
1697 resource) in enumerate(sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -08001698 if j:
1699 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001700 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -08001701 # Only list the GET ones.
1702 if method['httpMethod'] != 'GET':
1703 continue
Junji Watanabecb054042020-07-21 08:43:26 +00001704 print('- %s.%s: %s' % (resource_name, method_name, method['path']))
1705 print('\n'.join(' ' + l for l in textwrap.wrap(
1706 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +00001707 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -08001708 else:
1709 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001710 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -07001711 # Only list the GET ones.
1712 if method['httpMethod'] != 'GET':
1713 continue
Lei Leife202df2019-06-11 17:33:34 +00001714 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -08001715 print('\n'.join(
1716 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +00001717 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -07001718 return 0
1719
1720
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001721@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001722def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001723 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001724
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001725 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001726 """
1727 add_trigger_options(parser)
1728 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001729 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001730 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001731 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001732 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001733 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001734 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001735 except Failure as e:
Junji Watanabecb054042020-07-21 08:43:26 +00001736 on_error.report('Failed to trigger %s(%s): %s' %
1737 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001738 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001739 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001740 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001741 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001742 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001743 task_ids = [
Junji Watanabe38b28b02020-04-23 10:23:30 +00001744 t['task_id']
1745 for t in sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001746 ]
Caleb Rouleau779c4f02019-05-22 21:18:49 +00001747 for task_id in task_ids:
1748 print('Task: {server}/task?id={task}'.format(
1749 server=options.swarming, task=task_id))
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001750 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001751 offset = 0
1752 for s in task_request.task_slices:
Junji Watanabecb054042020-07-21 08:43:26 +00001753 m = (offset + s.properties.execution_timeout_secs + s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001754 if m > options.timeout:
1755 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001756 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001757 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001758 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001759 return collect(options.swarming, task_ids, options.timeout,
1760 options.decorate, options.print_status_updates,
1761 options.task_summary_json, options.task_output_dir,
1762 options.task_output_stdout, options.perf,
1763 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001764 except Failure:
1765 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001766 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001767
1768
maruel18122c62015-10-23 06:31:23 -07001769@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001770def CMDreproduce(parser, args):
1771 """Runs a task locally that was triggered on the server.
1772
1773 This running locally the same commands that have been run on the bot. The data
1774 downloaded will be in a subdirectory named 'work' of the current working
1775 directory.
maruel18122c62015-10-23 06:31:23 -07001776
1777 You can pass further additional arguments to the target command by passing
1778 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001779 """
maruelc070e672016-02-22 17:32:57 -08001780 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001781 '--output',
1782 metavar='DIR',
1783 default='out',
maruelc070e672016-02-22 17:32:57 -08001784 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001785 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001786 '--work',
1787 metavar='DIR',
1788 default='work',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001789 help='Directory to map the task input files into')
1790 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001791 '--cache',
1792 metavar='DIR',
1793 default='cache',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001794 help='Directory that contains the input cache')
1795 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001796 '--leak',
1797 action='store_true',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001798 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001799 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001800 extra_args = []
1801 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001802 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001803 if len(args) > 1:
1804 if args[1] == '--':
1805 if len(args) > 2:
1806 extra_args = args[2:]
1807 else:
1808 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001809
smut281c3902018-05-30 17:50:05 -07001810 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001811 request = net.url_read_json(url)
1812 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001813 print('Failed to retrieve request data for the task', file=sys.stderr)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001814 return 1
1815
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001816 workdir = six.text_type(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001817 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001818 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001819 fs.mkdir(workdir)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001820 cachedir = six.text_type(os.path.abspath('cipd_cache'))
iannucci31ab9192017-05-02 19:11:56 -07001821 if not fs.exists(cachedir):
1822 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001823
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001824 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001825 env = os.environ.copy()
1826 env['SWARMING_BOT_ID'] = 'reproduce'
1827 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001828 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001829 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001830 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001831 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001832 if not i['value']:
1833 env.pop(key, None)
1834 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001835 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001836
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001837 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001838 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001839 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001840 for i in env_prefixes:
1841 key = i['key']
1842 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001843 cur = env.get(key)
1844 if cur:
1845 paths.append(cur)
1846 env[key] = os.path.pathsep.join(paths)
1847
iannucci31ab9192017-05-02 19:11:56 -07001848 command = []
nodir152cba62016-05-12 16:08:56 -07001849 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001850 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001851 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +00001852 properties['inputs_ref']['isolatedserver'],
1853 properties['inputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001854 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001855 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1856 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1857 # leak.
1858 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001859 cache = local_caching.DiskContentAddressedCache(
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001860 six.text_type(os.path.abspath(options.cache)), policies, False)
maruel29ab2fd2015-10-16 11:44:01 -07001861 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001862 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001863 command = bundle.command
1864 if bundle.relative_cwd:
1865 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001866 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001867
1868 if properties.get('command'):
1869 command.extend(properties['command'])
1870
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001871 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Brian Sheedy7a761172019-08-30 22:55:14 +00001872 command = tools.find_executable(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001873 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001874 new_command = run_isolated.process_command(command, 'invalid', None)
1875 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001876 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001877 else:
1878 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001879 options.output = os.path.abspath(options.output)
Junji Watanabecb054042020-07-21 08:43:26 +00001880 new_command = run_isolated.process_command(command, options.output, None)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001881 if not os.path.isdir(options.output):
1882 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001883 command = new_command
1884 file_path.ensure_command_has_abs_path(command, workdir)
1885
1886 if properties.get('cipd_input'):
1887 ci = properties['cipd_input']
1888 cp = ci['client_package']
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001889 client_manager = cipd.get_client(cachedir, ci['server'], cp['package_name'],
1890 cp['version'])
iannucci31ab9192017-05-02 19:11:56 -07001891
1892 with client_manager as client:
1893 by_path = collections.defaultdict(list)
1894 for pkg in ci['packages']:
1895 path = pkg['path']
1896 # cipd deals with 'root' as ''
1897 if path == '.':
1898 path = ''
1899 by_path[path].append((pkg['package_name'], pkg['version']))
1900 client.ensure(workdir, by_path, cache_dir=cachedir)
1901
maruel77f720b2015-09-15 12:35:22 -07001902 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001903 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001904 except OSError as e:
Lei Leife202df2019-06-11 17:33:34 +00001905 print('Failed to run: %s' % ' '.join(command), file=sys.stderr)
1906 print(str(e), file=sys.stderr)
maruel77f720b2015-09-15 12:35:22 -07001907 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001908 finally:
1909 # Do not delete options.cache.
1910 if not options.leak:
1911 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001912
1913
maruel0eb1d1b2015-10-02 14:48:21 -07001914@subcommand.usage('bot_id')
1915def CMDterminate(parser, args):
1916 """Tells a bot to gracefully shut itself down as soon as it can.
1917
1918 This is done by completing whatever current task there is then exiting the bot
1919 process.
1920 """
1921 parser.add_option(
1922 '--wait', action='store_true', help='Wait for the bot to terminate')
1923 options, args = parser.parse_args(args)
1924 if len(args) != 1:
1925 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001926 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001927 request = net.url_read_json(url, data={})
1928 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001929 print('Failed to ask for termination', file=sys.stderr)
maruel0eb1d1b2015-10-02 14:48:21 -07001930 return 1
1931 if options.wait:
Junji Watanabecb054042020-07-21 08:43:26 +00001932 return collect(options.swarming, [request['task_id']], 0., False, False,
1933 None, None, [], False, None)
maruelbfc5f872017-06-10 16:43:17 -07001934 else:
Lei Leife202df2019-06-11 17:33:34 +00001935 print(request['task_id'])
maruel0eb1d1b2015-10-02 14:48:21 -07001936 return 0
1937
1938
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001939@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001940def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001941 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001942
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001943 Passes all extra arguments provided after '--' as additional command line
1944 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001945 """
1946 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001947 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001948 parser.add_option(
1949 '--dump-json',
1950 metavar='FILE',
1951 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001952 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001953 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001954 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001955 tasks = trigger_task_shards(options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001956 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001957 print('Triggered task: %s' % task_request.name)
Junji Watanabecb054042020-07-21 08:43:26 +00001958 tasks_sorted = sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001959 if options.dump_json:
1960 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001961 'base_task_name': task_request.name,
1962 'tasks': tasks,
1963 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001964 }
maruel46b015f2015-10-13 18:40:35 -07001965 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001966 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001967 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001968 (options.swarming, options.dump_json))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001969 else:
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 %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001972 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001973 print('Or visit:')
1974 for t in tasks_sorted:
1975 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001976 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001977 except Failure:
1978 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001979 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001980
1981
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001982class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +00001983
maruel@chromium.org0437a732013-08-27 16:05:52 +00001984 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001985 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001986 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001987 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001988 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001989 '-S',
1990 '--swarming',
1991 metavar='URL',
1992 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001993 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001994 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001995 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001996
1997 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001998 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001999 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002000 auth.process_auth_options(self, options)
2001 user = self._process_swarming(options)
2002 if hasattr(options, 'user') and not options.user:
2003 options.user = user
2004 return options, args
2005
2006 def _process_swarming(self, options):
2007 """Processes the --swarming option and aborts if not specified.
2008
2009 Returns the identity as determined by the server.
2010 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00002011 if not options.swarming:
2012 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002013 try:
2014 options.swarming = net.fix_url(options.swarming)
2015 except ValueError as e:
2016 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +00002017
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05002018 try:
2019 user = auth.ensure_logged_in(options.swarming)
2020 except ValueError as e:
2021 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002022 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00002023
2024
2025def main(args):
2026 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04002027 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002028
2029
2030if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07002031 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00002032 fix_encoding.fix_encoding()
2033 tools.disable_buffering()
2034 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +00002035 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002036 sys.exit(main(sys.argv[1:]))