blob: 520c3870c4a3f4781e4f751fa2cd1326c26043ed [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
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000041from utils import file_path
42from utils import fs
43from utils import logging_utils
44from utils import net
45from utils import on_error
46from utils import subprocess42
47from utils import threading_utils
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050048
49
50class Failure(Exception):
51 """Generic failure."""
52 pass
53
54
maruel0a25f6c2017-05-10 10:43:23 -070055def default_task_name(options):
56 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050057 if not options.task_name:
Junji Watanabe38b28b02020-04-23 10:23:30 +000058 task_name = u'%s/%s' % (options.user, '_'.join(
59 '%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070060 if options.isolated:
61 task_name += u'/' + options.isolated
62 return task_name
63 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050064
65
66### Triggering.
67
maruel77f720b2015-09-15 12:35:22 -070068# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000069CipdPackage = collections.namedtuple('CipdPackage', [
70 'package_name',
71 'path',
72 'version',
73])
borenet02f772b2016-06-22 12:42:19 -070074
borenet02f772b2016-06-22 12:42:19 -070075# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000076CipdInput = collections.namedtuple('CipdInput', [
77 'client_package',
78 'packages',
79 'server',
80])
borenet02f772b2016-06-22 12:42:19 -070081
82# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000083FilesRef = collections.namedtuple('FilesRef', [
84 'isolated',
85 'isolatedserver',
86 'namespace',
87])
maruel77f720b2015-09-15 12:35:22 -070088
89# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080090StringListPair = collections.namedtuple(
Junji Watanabe38b28b02020-04-23 10:23:30 +000091 'StringListPair',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000092 [
Junji Watanabe38b28b02020-04-23 10:23:30 +000093 'key',
94 'value', # repeated string
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000095 ])
96
Junji Watanabe38b28b02020-04-23 10:23:30 +000097# See ../appengine/swarming/swarming_rpcs.py.
98Containment = collections.namedtuple('Containment', [
Junji Watanabe38b28b02020-04-23 10:23:30 +000099 'containment_type',
100])
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800101
102# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +0000103TaskProperties = collections.namedtuple('TaskProperties', [
104 'caches',
105 'cipd_input',
106 'command',
107 'containment',
108 'relative_cwd',
109 'dimensions',
110 'env',
111 'env_prefixes',
112 'execution_timeout_secs',
113 'extra_args',
114 'grace_period_secs',
115 'idempotent',
116 'inputs_ref',
117 'io_timeout_secs',
118 'outputs',
119 'secret_bytes',
120])
maruel77f720b2015-09-15 12:35:22 -0700121
Junji Watanabecb054042020-07-21 08:43:26 +0000122# See ../appengine/swarming/swarming_rpcs.py.
123TaskSlice = collections.namedtuple('TaskSlice', [
124 'expiration_secs',
125 'properties',
126 'wait_for_capacity',
127])
maruel77f720b2015-09-15 12:35:22 -0700128
129# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabecb054042020-07-21 08:43:26 +0000130NewTaskRequest = collections.namedtuple('NewTaskRequest', [
131 'name',
132 'parent_task_id',
133 'priority',
Junji Watanabe71bbaef2020-07-21 08:55:37 +0000134 'realm',
Scott Lee44c13d72020-09-14 06:09:50 +0000135 'resultdb',
Junji Watanabecb054042020-07-21 08:43:26 +0000136 'task_slices',
137 'service_account',
138 'tags',
139 'user',
140 'pool_task_template',
141])
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500142
143
maruel77f720b2015-09-15 12:35:22 -0700144def namedtuple_to_dict(value):
145 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400146 if hasattr(value, '_asdict'):
147 return namedtuple_to_dict(value._asdict())
148 if isinstance(value, (list, tuple)):
149 return [namedtuple_to_dict(v) for v in value]
150 if isinstance(value, dict):
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000151 return {k: namedtuple_to_dict(v) for k, v in value.items()}
Lei Lei73a5f732020-03-23 20:36:14 +0000152 # json.dumps in Python3 doesn't support bytes.
153 if isinstance(value, bytes):
154 return six.ensure_str(value)
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400155 return value
maruel77f720b2015-09-15 12:35:22 -0700156
157
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700158def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800159 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700160
161 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500162 """
maruel77f720b2015-09-15 12:35:22 -0700163 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700164 # Don't send 'service_account' if it is None to avoid confusing older
165 # version of the server that doesn't know about 'service_account' and don't
166 # use it at all.
167 if not out['service_account']:
168 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000169 for task_slice in out['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +0000170 task_slice['properties']['env'] = [{
171 'key': k,
172 'value': v
173 } for k, v in task_slice['properties']['env'].items()]
Brad Hallf78187a2018-10-19 17:08:55 +0000174 task_slice['properties']['env'].sort(key=lambda x: x['key'])
Takuto Ikuta35250172020-01-31 09:33:46 +0000175 out['request_uuid'] = str(uuid.uuid4())
maruel77f720b2015-09-15 12:35:22 -0700176 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500177
178
maruel77f720b2015-09-15 12:35:22 -0700179def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500180 """Triggers a request on the Swarming server and returns the json data.
181
182 It's the low-level function.
183
184 Returns:
185 {
186 'request': {
187 'created_ts': u'2010-01-02 03:04:05',
188 'name': ..
189 },
190 'task_id': '12300',
191 }
192 """
193 logging.info('Triggering: %s', raw_request['name'])
194
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500195 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700196 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197 if not result:
198 on_error.report('Failed to trigger task %s' % raw_request['name'])
199 return None
maruele557bce2015-11-17 09:01:27 -0800200 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800201 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800202 msg = 'Failed to trigger task %s' % raw_request['name']
203 if result['error'].get('errors'):
204 for err in result['error']['errors']:
205 if err.get('message'):
206 msg += '\nMessage: %s' % err['message']
207 if err.get('debugInfo'):
208 msg += '\nDebug info:\n%s' % err['debugInfo']
209 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800210 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800211
212 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800213 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500214 return result
215
216
217def setup_googletest(env, shards, index):
218 """Sets googletest specific environment variables."""
219 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700220 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
221 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
222 env = env[:]
223 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
224 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500225 return env
226
227
Ye Kuang3f3f2f72020-10-21 10:14:59 +0000228def trigger_task_shards(swarming, task_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500229 """Triggers one or many subtasks of a sharded task.
230
231 Returns:
232 Dict with task details, returned to caller as part of --dump-json output.
233 None in case of failure.
234 """
Junji Watanabecb054042020-07-21 08:43:26 +0000235
Ye Kuang3f3f2f72020-10-21 10:14:59 +0000236 def convert_request():
Erik Chend50a88f2019-02-16 01:22:07 +0000237 """
238 Args:
239 index: The index of the task request.
240
241 Returns:
242 raw_request: A swarming compatible JSON dictionary of the request.
243 shard_index: The index of the shard, which may be different than the index
244 of the task request.
245 """
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700246 req = task_request_to_raw_request(task_request)
Ye Kuang3f3f2f72020-10-21 10:14:59 +0000247 shard_index = 0
Erik Chend50a88f2019-02-16 01:22:07 +0000248
Ye Kuang3f3f2f72020-10-21 10:14:59 +0000249 task_slices = req['task_slices']
250 total_shards = 1
251 # Multiple tasks slices might exist if there are optional "slices", e.g.
252 # multiple ways of dispatching the task that should be equivalent. These
253 # should be functionally equivalent but we have cannot guarantee that. If
254 # we see the GTEST_SHARD_INDEX env var, we assume that it applies to all
255 # slices.
256 for task_slice in task_slices:
257 for env_var in task_slice['properties']['env']:
258 if env_var['key'] == 'GTEST_SHARD_INDEX':
259 shard_index = int(env_var['value'])
260 if env_var['key'] == 'GTEST_TOTAL_SHARDS':
261 total_shards = int(env_var['value'])
262 if total_shards > 1:
263 req['name'] += ':%s:%s' % (shard_index, total_shards)
264 if shard_index and total_shards:
265 req['tags'] += [
266 'shard_index:%d' % shard_index,
267 'total_shards:%d' % total_shards,
268 ]
Erik Chend50a88f2019-02-16 01:22:07 +0000269
270 return req, shard_index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500271
Ye Kuang3f3f2f72020-10-21 10:14:59 +0000272 request, shard_index = convert_request()
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500273 tasks = {}
274 priority_warning = False
Ye Kuang3f3f2f72020-10-21 10:14:59 +0000275 task = swarming_trigger(swarming, request)
276 if task is not None:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500277 logging.info('Request result: %s', task)
278 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400279 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500280 priority_warning = True
Junji Watanabecb054042020-07-21 08:43:26 +0000281 print(
282 'Priority was reset to %s' % task['request']['priority'],
283 file=sys.stderr)
Ted Pudlikadc55e92020-09-28 23:25:49 +0000284 view_url = '%s/user/task/%s' % (swarming, task['task_id'])
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500285 tasks[request['name']] = {
Junji Watanabecb054042020-07-21 08:43:26 +0000286 'shard_index': shard_index,
287 'task_id': task['task_id'],
Ted Pudlikadc55e92020-09-28 23:25:49 +0000288 'view_url': view_url,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500289 }
Ted Pudlikadc55e92020-09-28 23:25:49 +0000290 logging.info('Task UI: %s', view_url)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500291
Ye Kuang3f3f2f72020-10-21 10:14:59 +0000292 if not tasks:
293 print('Task did not trigger successfully', file=sys.stderr)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500294 return None
295
296 return tasks
297
298
299### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000300
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700301# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000302STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700303
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400304
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000305class TaskState(object):
306 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000307
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000308 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
309 is the source of truth for these values:
310 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400311
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000312 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400313 """
314 RUNNING = 0x10
315 PENDING = 0x20
316 EXPIRED = 0x30
317 TIMED_OUT = 0x40
318 BOT_DIED = 0x50
319 CANCELED = 0x60
320 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400321 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400322 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400323
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000324 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400325
maruel77f720b2015-09-15 12:35:22 -0700326 _ENUMS = {
Junji Watanabecb054042020-07-21 08:43:26 +0000327 'RUNNING': RUNNING,
328 'PENDING': PENDING,
329 'EXPIRED': EXPIRED,
330 'TIMED_OUT': TIMED_OUT,
331 'BOT_DIED': BOT_DIED,
332 'CANCELED': CANCELED,
333 'COMPLETED': COMPLETED,
334 'KILLED': KILLED,
335 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700336 }
337
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400338 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700339 def from_enum(cls, state):
340 """Returns int value based on the string."""
341 if state not in cls._ENUMS:
342 raise ValueError('Invalid state %s' % state)
343 return cls._ENUMS[state]
344
maruel@chromium.org0437a732013-08-27 16:05:52 +0000345
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700346class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700347 """Assembles task execution summary (for --task-summary-json output).
348
349 Optionally fetches task outputs from isolate server to local disk (used when
350 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700351
352 This object is shared among multiple threads running 'retrieve_results'
353 function, in particular they call 'process_shard_result' method in parallel.
354 """
355
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000356 def __init__(self, task_output_dir, task_output_stdout, shard_count,
357 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700358 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
359
360 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700361 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362 shard_count: expected number of task shards.
363 """
maruel12e30012015-10-09 11:55:35 -0700364 self.task_output_dir = (
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000365 six.text_type(os.path.abspath(task_output_dir))
maruel12e30012015-10-09 11:55:35 -0700366 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000367 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000369 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370
371 self._lock = threading.Lock()
372 self._per_shard_results = {}
373 self._storage = None
374
nodire5028a92016-04-29 14:38:21 -0700375 if self.task_output_dir:
376 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377
Vadim Shtayurab450c602014-05-12 19:23:25 -0700378 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700379 """Stores results of a single task shard, fetches output files if necessary.
380
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400381 Modifies |result| in place.
382
maruel77f720b2015-09-15 12:35:22 -0700383 shard_index is 0-based.
384
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 Called concurrently from multiple threads.
386 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700388 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700389 if shard_index < 0 or shard_index >= self.shard_count:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000390 logging.warning('Shard index %d is outside of expected range: [0; %d]',
391 shard_index, self.shard_count - 1)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700392 return
393
maruel77f720b2015-09-15 12:35:22 -0700394 if result.get('outputs_ref'):
395 ref = result['outputs_ref']
396 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
397 ref['isolatedserver'],
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000398 urllib.parse.urlencode([('namespace', ref['namespace']),
399 ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400400
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700401 # Store result dict of that shard, ignore results we've already seen.
402 with self._lock:
403 if shard_index in self._per_shard_results:
404 logging.warning('Ignoring duplicate shard index %d', shard_index)
405 return
406 self._per_shard_results[shard_index] = result
407
408 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700409 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000410 server_ref = isolate_storage.ServerRef(
Junji Watanabecb054042020-07-21 08:43:26 +0000411 result['outputs_ref']['isolatedserver'],
412 result['outputs_ref']['namespace'])
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000413 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400414 if storage:
415 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400416 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
417 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400418 isolateserver.fetch_isolated(
Junji Watanabecb054042020-07-21 08:43:26 +0000419 result['outputs_ref']['isolated'], storage,
Lei Leife202df2019-06-11 17:33:34 +0000420 local_caching.MemoryContentAddressedCache(file_mode_mask=0o700),
Junji Watanabecb054042020-07-21 08:43:26 +0000421 os.path.join(self.task_output_dir, str(shard_index)), False,
422 self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700423
424 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700425 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700426 with self._lock:
427 # Write an array of shard results with None for missing shards.
428 summary = {
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000429 'shards': [
430 self._per_shard_results.get(i) for i in range(self.shard_count)
431 ],
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700432 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000433
434 # Don't store stdout in the summary if not requested too.
435 if "json" not in self.task_output_stdout:
436 for shard_json in summary['shards']:
437 if not shard_json:
438 continue
439 if "output" in shard_json:
440 del shard_json["output"]
441 if "outputs" in shard_json:
442 del shard_json["outputs"]
443
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700444 # Write summary.json to task_output_dir as well.
445 if self.task_output_dir:
446 tools.write_json(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000447 os.path.join(self.task_output_dir, u'summary.json'), summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 if self._storage:
449 self._storage.close()
450 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700451 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000453 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700455 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 with self._lock:
457 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000458 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700459 else:
460 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000461 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700462 logging.error(
463 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000464 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700465 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000466 if self._storage.server_ref.namespace != server_ref.namespace:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000467 logging.error('Task shards are using multiple namespaces: %s and %s',
468 self._storage.server_ref.namespace,
469 server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700470 return None
471 return self._storage
472
473
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500474def now():
475 """Exists so it can be mocked easily."""
476 return time.time()
477
478
maruel77f720b2015-09-15 12:35:22 -0700479def parse_time(value):
480 """Converts serialized time from the API to datetime.datetime."""
481 # When microseconds are 0, the '.123456' suffix is elided. This means the
482 # serialized format is not consistent, which confuses the hell out of python.
483 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
484 try:
485 return datetime.datetime.strptime(value, fmt)
486 except ValueError:
487 pass
488 raise ValueError('Failed to parse %s' % value)
489
490
Junji Watanabe38b28b02020-04-23 10:23:30 +0000491def retrieve_results(base_url, shard_index, task_id, timeout, should_stop,
492 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400493 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494
Vadim Shtayurab450c602014-05-12 19:23:25 -0700495 Returns:
496 <result dict> on success.
497 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700498 """
maruel71c61c82016-02-22 06:52:05 -0800499 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700500 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700501 if include_perf:
502 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700503 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700504 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400505 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700506 attempt = 0
507
508 while not should_stop.is_set():
509 attempt += 1
510
511 # Waiting for too long -> give up.
512 current_time = now()
513 if deadline and current_time >= deadline:
Junji Watanabecb054042020-07-21 08:43:26 +0000514 logging.error('retrieve_results(%s) timed out on attempt %d', base_url,
515 attempt)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700516 return None
517
518 # Do not spin too fast. Spin faster at the beginning though.
519 # Start with 1 sec delay and for each 30 sec of waiting add another second
520 # of delay, until hitting 15 sec ceiling.
521 if attempt > 1:
522 max_delay = min(15, 1 + (current_time - started) / 30.0)
523 delay = min(max_delay, deadline - current_time) if deadline else max_delay
524 if delay > 0:
525 logging.debug('Waiting %.1f sec before retrying', delay)
526 should_stop.wait(delay)
527 if should_stop.is_set():
528 return None
529
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400530 # Disable internal retries in net.url_read_json, since we are doing retries
531 # ourselves.
532 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700533 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
534 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400535 # Retry on 500s only if no timeout is specified.
536 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400537 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400538 if timeout == -1:
539 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400540 continue
maruel77f720b2015-09-15 12:35:22 -0700541
maruelbf53e042015-12-01 15:00:51 -0800542 if result.get('error'):
543 # An error occurred.
544 if result['error'].get('errors'):
545 for err in result['error']['errors']:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000546 logging.warning('Error while reading task: %s; %s',
547 err.get('message'), err.get('debugInfo'))
maruelbf53e042015-12-01 15:00:51 -0800548 elif result['error'].get('message'):
Junji Watanabecb054042020-07-21 08:43:26 +0000549 logging.warning('Error while reading task: %s',
550 result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400551 if timeout == -1:
552 return result
maruelbf53e042015-12-01 15:00:51 -0800553 continue
554
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400555 # When timeout == -1, always return on first attempt. 500s are already
556 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000557 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000558 if fetch_stdout:
559 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700560 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700561 # Record the result, try to fetch attached output files (if any).
562 if output_collector:
563 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700564 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700565 if result.get('internal_failure'):
566 logging.error('Internal error!')
567 elif result['state'] == 'BOT_DIED':
568 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700569 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000570
571
Junji Watanabecb054042020-07-21 08:43:26 +0000572def yield_results(swarm_base_url, task_ids, timeout, max_threads,
573 print_status_updates, output_collector, include_perf,
574 fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500575 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000576
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700577 Duplicate shards are ignored. Shards are yielded in order of completion.
578 Timed out shards are NOT yielded at all. Caller can compare number of yielded
579 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000580
581 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500582 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 +0000583 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500584
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700585 output_collector is an optional instance of TaskOutputCollector that will be
586 used to fetch files produced by a task from isolate server to the local disk.
587
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500588 Yields:
589 (index, result). In particular, 'result' is defined as the
590 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000591 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000592 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400593 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700594 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700595 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700596
maruel@chromium.org0437a732013-08-27 16:05:52 +0000597 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
598 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700599 # Adds a task to the thread pool to call 'retrieve_results' and return
600 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400601 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000602 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700603 task_fn = lambda *args: (shard_index, retrieve_results(*args))
Junji Watanabecb054042020-07-21 08:43:26 +0000604 pool.add_task(0, results_channel.wrap_task(task_fn), swarm_base_url,
605 shard_index, task_id, timeout, should_stop,
606 output_collector, include_perf, fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700607
608 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400609 for shard_index, task_id in enumerate(task_ids):
610 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700611
612 # Wait for all of them to finish.
Lei Lei73a5f732020-03-23 20:36:14 +0000613 # Convert to list, since range in Python3 doesn't have remove.
614 shards_remaining = list(range(len(task_ids)))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400615 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700616 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700617 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700618 try:
Marc-Antoine Ruel4494b6c2018-11-28 21:00:41 +0000619 shard_index, result = results_channel.next(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700620 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700621 except threading_utils.TaskChannel.Timeout:
622 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000623 time_now = str(datetime.datetime.now())
624 _, time_now = time_now.split(' ')
Junji Watanabe38b28b02020-04-23 10:23:30 +0000625 print('%s '
626 'Waiting for results from the following shards: %s' %
627 (time_now, ', '.join(map(str, shards_remaining))))
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700628 sys.stdout.flush()
629 continue
630 except Exception:
631 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700632
633 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700634 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000635 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500636 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000637 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700638
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639 # Yield back results to the caller.
640 assert shard_index in shards_remaining
641 shards_remaining.remove(shard_index)
642 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643
maruel@chromium.org0437a732013-08-27 16:05:52 +0000644 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700645 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000646 should_stop.set()
647
648
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000649def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700651 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Junji Watanabecb054042020-07-21 08:43:26 +0000652 pending = '%.1fs' % (parse_time(metadata['started_ts']) -
653 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400654 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
655 metadata.get('abandoned_ts')):
Junji Watanabecb054042020-07-21 08:43:26 +0000656 pending = '%.1fs' % (parse_time(metadata['abandoned_ts']) -
657 parse_time(metadata['created_ts'])).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400658 else:
659 pending = 'N/A'
660
maruel77f720b2015-09-15 12:35:22 -0700661 if metadata.get('duration') is not None:
662 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400663 else:
664 duration = 'N/A'
665
maruel77f720b2015-09-15 12:35:22 -0700666 if metadata.get('exit_code') is not None:
667 # Integers are encoded as string to not loose precision.
668 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400669 else:
670 exit_code = 'N/A'
671
672 bot_id = metadata.get('bot_id') or 'N/A'
673
maruel77f720b2015-09-15 12:35:22 -0700674 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400675 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000676 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400677 if metadata.get('state') == 'CANCELED':
678 tag_footer2 = ' Pending: %s CANCELED' % pending
679 elif metadata.get('state') == 'EXPIRED':
680 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400681 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400682 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
683 pending, duration, bot_id, exit_code, metadata['state'])
684 else:
685 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
686 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400687
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000688 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
689 dash_pad = '+-%s-+' % ('-' * tag_len)
690 tag_header = '| %s |' % tag_header.ljust(tag_len)
691 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
692 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400693
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000694 if include_stdout:
695 return '\n'.join([
696 dash_pad,
697 tag_header,
698 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400699 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000700 dash_pad,
701 tag_footer1,
702 tag_footer2,
703 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000704 ])
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000705 return '\n'.join([
706 dash_pad,
707 tag_header,
708 tag_footer2,
709 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000710 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000711
712
Junji Watanabecb054042020-07-21 08:43:26 +0000713def collect(swarming, task_ids, timeout, decorate, print_status_updates,
714 task_summary_json, task_output_dir, task_output_stdout,
715 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700716 """Retrieves results of a Swarming task.
717
718 Returns:
719 process exit code that should be returned to the user.
720 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000721
722 filter_cb = None
723 if filepath_filter:
724 filter_cb = re.compile(filepath_filter).match
725
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700726 # Collect summary JSON and output files (if task_output_dir is not None).
Junji Watanabecb054042020-07-21 08:43:26 +0000727 output_collector = TaskOutputCollector(task_output_dir, task_output_stdout,
728 len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700729
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700730 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700731 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400732 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700733 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400734 for index, metadata in yield_results(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000735 swarming,
736 task_ids,
737 timeout,
738 None,
739 print_status_updates,
740 output_collector,
741 include_perf,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000742 (len(task_output_stdout) > 0),
Junji Watanabe38b28b02020-04-23 10:23:30 +0000743 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700744 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700745
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400746 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700747 shard_exit_code = metadata.get('exit_code')
748 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700749 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700750 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700751 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400752 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700753 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700754
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700755 if decorate:
Lei Lei805a75d2020-10-08 16:31:55 +0000756 s = decorate_shard_output(swarming, index, metadata,
Lei Lei73a5f732020-03-23 20:36:14 +0000757 "console" in task_output_stdout).encode(
Lei Lei805a75d2020-10-08 16:31:55 +0000758 'utf-8', 'replace')
759
760 # The default system encoding is ascii, which can not handle non-ascii
761 # characters, switch to use sys.stdout.buffer.write in Python3 to
762 # send utf-8 to stdout regardless of the console's encoding.
763 if six.PY3:
764 sys.stdout.buffer.write(s)
765 else:
766 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400767 if len(seen_shards) < len(task_ids):
768 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769 else:
Junji Watanabecb054042020-07-21 08:43:26 +0000770 print('%s: %s %s' % (metadata.get(
771 'bot_id', 'N/A'), metadata['task_id'], shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000772 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700773 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400774 if output:
775 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700776 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700777 summary = output_collector.finalize()
778 if task_summary_json:
779 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700780
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400781 if decorate and total_duration:
782 print('Total duration: %.1fs' % total_duration)
783
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400784 if len(seen_shards) != len(task_ids):
785 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Junji Watanabecb054042020-07-21 08:43:26 +0000786 print(
787 'Results from some shards are missing: %s' %
788 ', '.join(map(str, missing_shards)),
789 file=sys.stderr)
Vadim Shtayurac524f512014-05-15 09:54:56 -0700790 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700791
maruela5490782015-09-30 10:56:59 -0700792 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000793
794
maruel77f720b2015-09-15 12:35:22 -0700795### API management.
796
797
798class APIError(Exception):
799 pass
800
801
802def endpoints_api_discovery_apis(host):
803 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
804 the APIs exposed by a host.
805
806 https://developers.google.com/discovery/v1/reference/apis/list
807 """
maruel380e3262016-08-31 16:10:06 -0700808 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
809 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700810 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
811 if data is None:
812 raise APIError('Failed to discover APIs on %s' % host)
813 out = {}
814 for api in data['items']:
815 if api['id'] == 'discovery:v1':
816 continue
817 # URL is of the following form:
818 # url = host + (
819 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
820 api_data = net.url_read_json(api['discoveryRestUrl'])
821 if api_data is None:
822 raise APIError('Failed to discover %s on %s' % (api['id'], host))
823 out[api['id']] = api_data
824 return out
825
826
maruelaf6b06c2017-06-08 06:26:53 -0700827def get_yielder(base_url, limit):
828 """Returns the first query and a function that yields following items."""
829 CHUNK_SIZE = 250
830
831 url = base_url
832 if limit:
833 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
834 data = net.url_read_json(url)
835 if data is None:
836 # TODO(maruel): Do basic diagnostic.
837 raise Failure('Failed to access %s' % url)
838 org_cursor = data.pop('cursor', None)
839 org_total = len(data.get('items') or [])
840 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
841 if not org_cursor or not org_total:
842 # This is not an iterable resource.
843 return data, lambda: []
844
845 def yielder():
846 cursor = org_cursor
847 total = org_total
848 # Some items support cursors. Try to get automatically if cursors are needed
849 # by looking at the 'cursor' items.
850 while cursor and (not limit or total < limit):
851 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000852 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -0700853 if limit:
854 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
855 new = net.url_read_json(url)
856 if new is None:
857 raise Failure('Failed to access %s' % url)
858 cursor = new.get('cursor')
859 new_items = new.get('items')
860 nb_items = len(new_items or [])
861 total += nb_items
862 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
863 yield new_items
864
865 return data, yielder
866
867
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500868### Commands.
869
870
871def abort_task(_swarming, _manifest):
872 """Given a task manifest that was triggered, aborts its execution."""
873 # TODO(vadimsh): No supported by the server yet.
874
875
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400876def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800877 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500878 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000879 '-d',
880 '--dimension',
881 default=[],
882 action='append',
883 nargs=2,
884 dest='dimensions',
885 metavar='FOO bar',
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500886 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000887 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +0000888 '--optional-dimension',
889 default=[],
890 action='append',
891 nargs=3,
892 dest='optional_dimensions',
893 metavar='key value expiration',
Brad Hallf78187a2018-10-19 17:08:55 +0000894 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500895 parser.add_option_group(parser.filter_group)
896
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400897
Brad Hallf78187a2018-10-19 17:08:55 +0000898def _validate_filter_option(parser, key, value, expiration, argname):
899 if ':' in key:
900 parser.error('%s key cannot contain ":"' % argname)
901 if key.strip() != key:
902 parser.error('%s key has whitespace' % argname)
903 if not key:
904 parser.error('%s key is empty' % argname)
905
906 if value.strip() != value:
907 parser.error('%s value has whitespace' % argname)
908 if not value:
909 parser.error('%s value is empty' % argname)
910
911 if expiration is not None:
912 try:
913 expiration = int(expiration)
914 except ValueError:
915 parser.error('%s expiration is not an integer' % argname)
916 if expiration <= 0:
917 parser.error('%s expiration should be positive' % argname)
918 if expiration % 60 != 0:
919 parser.error('%s expiration is not divisible by 60' % argname)
920
921
maruelaf6b06c2017-06-08 06:26:53 -0700922def process_filter_options(parser, options):
923 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000924 _validate_filter_option(parser, key, value, None, 'dimension')
925 for key, value, exp in options.optional_dimensions:
926 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700927 options.dimensions.sort()
928
929
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400930def add_trigger_options(parser):
931 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500932 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400933 add_filter_options(parser)
934
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400935 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800936 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000937 '-s',
938 '--isolated',
939 metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500940 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800941 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000942 '-e',
943 '--env',
944 default=[],
945 action='append',
946 nargs=2,
947 metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700948 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800949 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000950 '--env-prefix',
951 default=[],
952 action='append',
953 nargs=2,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800954 metavar='VAR local/path',
955 help='Prepend task-relative `local/path` to the task\'s VAR environment '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000956 'variable using os-appropriate pathsep character. Can be specified '
957 'multiple times for the same VAR to add multiple paths.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800958 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000959 '--idempotent',
960 action='store_true',
961 default=False,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400962 help='When set, the server will actively try to find a previous task '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000963 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800964 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000965 '--secret-bytes-path',
966 metavar='FILE',
Stephen Martinisf391c772019-02-01 01:22:12 +0000967 help='The optional path to a file containing the secret_bytes to use '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000968 'with this task.')
maruel681d6802017-01-17 16:56:03 -0800969 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000970 '--hard-timeout',
971 type='int',
972 default=60 * 60,
973 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400974 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800975 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000976 '--io-timeout',
977 type='int',
978 default=20 * 60,
979 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400980 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +0000981 containment_choices = ('NONE', 'AUTO', 'JOB_OBJECT')
982 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000983 '--containment-type',
984 default='NONE',
985 metavar='NONE',
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +0000986 choices=containment_choices,
987 help='Containment to use; one of: %s' % ', '.join(containment_choices))
maruel681d6802017-01-17 16:56:03 -0800988 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000989 '--raw-cmd',
990 action='store_true',
991 default=False,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500992 help='When set, the command after -- is used as-is without run_isolated. '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000993 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800994 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500995 '--relative-cwd',
996 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000997 'requires --raw-cmd')
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500998 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000999 '--cipd-package',
1000 action='append',
1001 default=[],
1002 metavar='PKG',
maruel5475ba62017-05-31 15:35:47 -07001003 help='CIPD packages to install on the Swarming bot. Uses the format: '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001004 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001005 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001006 '--named-cache',
1007 action='append',
1008 nargs=2,
1009 default=[],
maruel5475ba62017-05-31 15:35:47 -07001010 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001011 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1012 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001013 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001014 help='Email of a service account to run the task as, or literal "bot" '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001015 'string to indicate that the task should use the same account the '
1016 'bot itself is using to authenticate to Swarming. Don\'t use task '
1017 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001018 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001019 '--pool-task-template',
1020 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1021 default='AUTO',
1022 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001023 'By default, the pool\'s TaskTemplate is automatically selected, '
1024 'according the pool configuration on the server. Choices are: '
1025 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
Robert Iannuccifafa7352018-06-13 17:08:17 +00001026 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001027 '-o',
1028 '--output',
1029 action='append',
1030 default=[],
1031 metavar='PATH',
maruel5475ba62017-05-31 15:35:47 -07001032 help='A list of files to return in addition to those written to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001033 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1034 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001035 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001036 '--wait-for-capacity',
1037 action='store_true',
1038 default=False,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001039 help='Instructs to leave the task PENDING even if there\'s no known bot '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001040 'that could run this task, otherwise the task will be denied with '
1041 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001042 parser.add_option_group(group)
1043
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001044 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001045 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001046 '--priority',
1047 type='int',
1048 default=200,
maruel681d6802017-01-17 16:56:03 -08001049 help='The lower value, the more important the task is')
1050 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001051 '-T',
1052 '--task-name',
1053 metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001054 help='Display name of the task. Defaults to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001055 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1056 'isolated file is provided, if a hash is provided, it defaults to '
1057 '<user>/<dimensions>/<isolated hash>/<timestamp>')
maruel681d6802017-01-17 16:56:03 -08001058 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001059 '--tags',
1060 action='append',
1061 default=[],
1062 metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001063 help='Tags to assign to the task.')
1064 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001065 '--user',
1066 default='',
maruel681d6802017-01-17 16:56:03 -08001067 help='User associated with the task. Defaults to authenticated user on '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001068 'the server.')
maruel681d6802017-01-17 16:56:03 -08001069 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001070 '--expiration',
1071 type='int',
1072 default=6 * 60 * 60,
1073 metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001074 help='Seconds to allow the task to be pending for a bot to run before '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001075 'this task request expires.')
maruel681d6802017-01-17 16:56:03 -08001076 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001077 '--deadline', type='int', dest='expiration', help=optparse.SUPPRESS_HELP)
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001078 group.add_option(
1079 '--realm',
1080 dest='realm',
1081 metavar='REALM',
1082 help='Realm associated with the task.')
Scott Lee44c13d72020-09-14 06:09:50 +00001083 group.add_option(
1084 '--resultdb',
1085 action='store_true',
1086 default=False,
1087 help='When set, the task is created with ResultDB enabled.')
maruel681d6802017-01-17 16:56:03 -08001088 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001089
1090
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001091def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001092 """Processes trigger options and does preparatory steps.
1093
1094 Returns:
1095 NewTaskRequest instance.
1096 """
maruelaf6b06c2017-06-08 06:26:53 -07001097 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001098 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001099 if args and args[0] == '--':
1100 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001101
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001102 if not options.dimensions:
1103 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001104 if not any(k == 'pool' for k, _v in options.dimensions):
1105 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001106 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1107 parser.error('--tags must be in the format key:value')
1108 if options.raw_cmd and not args:
1109 parser.error(
1110 'Arguments with --raw-cmd should be passed after -- as command '
1111 'delimiter.')
1112 if options.isolate_server and not options.namespace:
1113 parser.error(
1114 '--namespace must be a valid value when --isolate-server is used')
1115 if not options.isolated and not options.raw_cmd:
1116 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1117
1118 # Isolated
1119 # --isolated is required only if --raw-cmd wasn't provided.
1120 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1121 # preferred server.
Takuto Ikutaae767b32020-05-11 01:22:19 +00001122 isolateserver.process_isolate_server_options(parser, options,
1123 not options.raw_cmd)
maruel0a25f6c2017-05-10 10:43:23 -07001124 inputs_ref = None
1125 if options.isolate_server:
1126 inputs_ref = FilesRef(
1127 isolated=options.isolated,
1128 isolatedserver=options.isolate_server,
1129 namespace=options.namespace)
1130
1131 # Command
1132 command = None
1133 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001134 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001135 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001136 if options.relative_cwd:
1137 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1138 if not a.startswith(os.getcwd()):
1139 parser.error(
1140 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001141 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001142 if options.relative_cwd:
1143 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001144 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001145
maruel0a25f6c2017-05-10 10:43:23 -07001146 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001147 cipd_packages = []
1148 for p in options.cipd_package:
1149 split = p.split(':', 2)
1150 if len(split) != 3:
1151 parser.error('CIPD packages must take the form: path:package:version')
Junji Watanabe38b28b02020-04-23 10:23:30 +00001152 cipd_packages.append(
1153 CipdPackage(package_name=split[1], path=split[0], version=split[2]))
borenet02f772b2016-06-22 12:42:19 -07001154 cipd_input = None
1155 if cipd_packages:
1156 cipd_input = CipdInput(
Junji Watanabecb054042020-07-21 08:43:26 +00001157 client_package=None, packages=cipd_packages, server=None)
borenet02f772b2016-06-22 12:42:19 -07001158
maruel0a25f6c2017-05-10 10:43:23 -07001159 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001160 secret_bytes = None
1161 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001162 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001163 secret_bytes = f.read().encode('base64')
1164
maruel0a25f6c2017-05-10 10:43:23 -07001165 # Named caches
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001166 caches = [{
1167 u'name': six.text_type(i[0]),
1168 u'path': six.text_type(i[1])
1169 } for i in options.named_cache]
maruel0a25f6c2017-05-10 10:43:23 -07001170
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001171 env_prefixes = {}
1172 for k, v in options.env_prefix:
1173 env_prefixes.setdefault(k, []).append(v)
1174
Brad Hallf78187a2018-10-19 17:08:55 +00001175 # Get dimensions into the key/value format we can manipulate later.
Junji Watanabecb054042020-07-21 08:43:26 +00001176 orig_dims = [{
1177 'key': key,
1178 'value': value
1179 } for key, value in options.dimensions]
Brad Hallf78187a2018-10-19 17:08:55 +00001180 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1181
1182 # Construct base properties that we will use for all the slices, adding in
1183 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001184 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001185 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001186 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001187 command=command,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001188 containment=Containment(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001189 containment_type=options.containment_type,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001190 ),
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001191 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001192 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001193 env=options.env,
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001194 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.items()],
maruel77f720b2015-09-15 12:35:22 -07001195 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001196 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001197 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001198 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001199 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001200 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001201 outputs=options.output,
1202 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001203
1204 slices = []
1205
1206 # Group the optional dimensions by expiration.
1207 dims_by_exp = {}
1208 for key, value, exp_secs in options.optional_dimensions:
Junji Watanabecb054042020-07-21 08:43:26 +00001209 dims_by_exp.setdefault(int(exp_secs), []).append({
1210 'key': key,
1211 'value': value
1212 })
Brad Hallf78187a2018-10-19 17:08:55 +00001213
1214 # Create the optional slices with expiration deltas, we fix up the properties
1215 # below.
1216 last_exp = 0
1217 for expiration_secs in sorted(dims_by_exp):
1218 t = TaskSlice(
1219 expiration_secs=expiration_secs - last_exp,
1220 properties=properties,
1221 wait_for_capacity=False)
1222 slices.append(t)
1223 last_exp = expiration_secs
1224
1225 # Add back in the default slice (the last one).
1226 exp = max(int(options.expiration) - last_exp, 60)
1227 base_task_slice = TaskSlice(
1228 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001229 properties=properties,
1230 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001231 slices.append(base_task_slice)
1232
Brad Hall7f463e62018-11-16 16:13:30 +00001233 # Add optional dimensions to the task slices, replacing a dimension that
1234 # has the same key if it is a dimension where repeating isn't valid (otherwise
1235 # we append it). Currently the only dimension we can repeat is "caches"; the
1236 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001237 extra_dims = []
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001238 for i, (_, kvs) in enumerate(sorted(dims_by_exp.items(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001239 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001240 # Replace or append the key/value pairs for this expiration in extra_dims;
1241 # we keep extra_dims around because we are iterating backwards and filling
1242 # in slices with shorter expirations. Dimensions expire as time goes on so
1243 # the slices that expire earlier will generally have more dimensions.
1244 for kv in kvs:
1245 if kv['key'] == 'caches':
1246 extra_dims.append(kv)
1247 else:
1248 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1249 # Then, add all the optional dimensions to the original dimension set, again
1250 # replacing if needed.
1251 for kv in extra_dims:
1252 if kv['key'] == 'caches':
1253 dims.append(kv)
1254 else:
1255 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001256 dims.sort(key=lambda x: (x['key'], x['value']))
1257 slice_properties = properties._replace(dimensions=dims)
1258 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1259
maruel77f720b2015-09-15 12:35:22 -07001260 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001261 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001262 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001263 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001264 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001265 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001266 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001267 user=options.user,
Junji Watanabe71bbaef2020-07-21 08:55:37 +00001268 pool_task_template=options.pool_task_template,
Scott Lee44c13d72020-09-14 06:09:50 +00001269 realm=options.realm,
1270 resultdb={'enable': options.resultdb})
maruel@chromium.org0437a732013-08-27 16:05:52 +00001271
1272
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001273class TaskOutputStdoutOption(optparse.Option):
1274 """Where to output the each task's console output (stderr/stdout).
1275
1276 The output will be;
1277 none - not be downloaded.
1278 json - stored in summary.json file *only*.
1279 console - shown on stdout *only*.
1280 all - stored in summary.json and shown on stdout.
1281 """
1282
1283 choices = ['all', 'json', 'console', 'none']
1284
1285 def __init__(self, *args, **kw):
1286 optparse.Option.__init__(
1287 self,
1288 *args,
1289 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001290 default=['console', 'json'],
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001291 help=re.sub(r'\s\s*', ' ', self.__doc__),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001292 **kw)
1293
1294 def convert_value(self, opt, value):
1295 if value not in self.choices:
Junji Watanabecb054042020-07-21 08:43:26 +00001296 raise optparse.OptionValueError(
1297 "%s must be one of %s not %r" %
1298 (self.get_opt_string(), self.choices, value))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001299 stdout_to = []
1300 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001301 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001302 elif value != 'none':
1303 stdout_to = [value]
1304 return stdout_to
1305
1306
maruel@chromium.org0437a732013-08-27 16:05:52 +00001307def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001308 parser.server_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001309 '-t',
1310 '--timeout',
1311 type='float',
1312 default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001313 help='Timeout to wait for result, set to -1 for no timeout and get '
Junji Watanabecb054042020-07-21 08:43:26 +00001314 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001315 parser.group_logging.add_option(
1316 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001317 parser.group_logging.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001318 '--print-status-updates',
1319 action='store_true',
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001320 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001321 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001322 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001323 '--task-summary-json',
1324 metavar='FILE',
1325 help='Dump a summary of task results to this file as json. It contains '
Junji Watanabecb054042020-07-21 08:43:26 +00001326 'only shards statuses as know to server directly. Any output files '
1327 'emitted by the task can be collected by using --task-output-dir')
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001328 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001329 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001330 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001331 help='Directory to put task results into. When the task finishes, this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001332 'directory contains per-shard directory with output files produced '
1333 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001334 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001335 TaskOutputStdoutOption('--task-output-stdout'))
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001336 parser.task_output_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001337 '--filepath-filter',
1338 help='This is regexp filter used to specify downloaded filepath when '
1339 'collecting isolated output.')
1340 parser.task_output_group.add_option(
1341 '--perf',
1342 action='store_true',
1343 default=False,
maruel9531ce02016-04-13 06:11:23 -07001344 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001345 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001346
1347
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001348def process_collect_options(parser, options):
1349 # Only negative -1 is allowed, disallow other negative values.
1350 if options.timeout != -1 and options.timeout < 0:
1351 parser.error('Invalid --timeout value')
1352
1353
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001354@subcommand.usage('bots...')
1355def CMDbot_delete(parser, args):
1356 """Forcibly deletes bots from the Swarming server."""
1357 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001358 '-f',
1359 '--force',
1360 action='store_true',
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001361 help='Do not prompt for confirmation')
1362 options, args = parser.parse_args(args)
1363 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001364 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001365
1366 bots = sorted(args)
1367 if not options.force:
1368 print('Delete the following bots?')
1369 for bot in bots:
1370 print(' %s' % bot)
1371 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1372 print('Goodbye.')
1373 return 1
1374
1375 result = 0
1376 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001377 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001378 if net.url_read_json(url, data={}, method='POST') is None:
1379 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001380 result = 1
1381 return result
1382
1383
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001384def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001385 """Returns information about the bots connected to the Swarming server."""
1386 add_filter_options(parser)
1387 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001388 '--dead-only',
1389 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001390 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001391 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001392 '-k',
1393 '--keep-dead',
1394 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001395 help='Keep both dead and alive bots')
1396 parser.filter_group.add_option(
1397 '--busy', action='store_true', help='Keep only busy bots')
1398 parser.filter_group.add_option(
1399 '--idle', action='store_true', help='Keep only idle bots')
1400 parser.filter_group.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001401 '-b', '--bare', action='store_true', help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001402 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001403 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001404
1405 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001406 parser.error('Use only one of --keep-dead or --dead-only')
1407 if options.busy and options.idle:
1408 parser.error('Use only one of --busy or --idle')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001409
smut281c3902018-05-30 17:50:05 -07001410 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001411 values = []
1412 if options.dead_only:
1413 values.append(('is_dead', 'TRUE'))
1414 elif options.keep_dead:
1415 values.append(('is_dead', 'NONE'))
1416 else:
1417 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001418
maruelaf6b06c2017-06-08 06:26:53 -07001419 if options.busy:
1420 values.append(('is_busy', 'TRUE'))
1421 elif options.idle:
1422 values.append(('is_busy', 'FALSE'))
1423 else:
1424 values.append(('is_busy', 'NONE'))
1425
maruelaf6b06c2017-06-08 06:26:53 -07001426 for key, value in options.dimensions:
1427 values.append(('dimensions', '%s:%s' % (key, value)))
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +00001428 url += urllib.parse.urlencode(values)
maruelaf6b06c2017-06-08 06:26:53 -07001429 try:
1430 data, yielder = get_yielder(url, 0)
1431 bots = data.get('items') or []
1432 for items in yielder():
1433 if items:
1434 bots.extend(items)
1435 except Failure as e:
1436 sys.stderr.write('\n%s\n' % e)
1437 return 1
maruel77f720b2015-09-15 12:35:22 -07001438 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Lei Leife202df2019-06-11 17:33:34 +00001439 print(bot['bot_id'])
maruelaf6b06c2017-06-08 06:26:53 -07001440 if not options.bare:
1441 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Lei Leife202df2019-06-11 17:33:34 +00001442 print(' %s' % json.dumps(dimensions, sort_keys=True))
maruelaf6b06c2017-06-08 06:26:53 -07001443 if bot.get('task_id'):
Lei Leife202df2019-06-11 17:33:34 +00001444 print(' task: %s' % bot['task_id'])
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001445 return 0
1446
1447
maruelfd0a90c2016-06-10 11:51:10 -07001448@subcommand.usage('task_id')
1449def CMDcancel(parser, args):
1450 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001451 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001452 '-k',
1453 '--kill-running',
1454 action='store_true',
1455 default=False,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001456 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001457 options, args = parser.parse_args(args)
1458 if not args:
1459 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001460 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001461 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001462 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001463 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001464 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001465 print('Deleting %s failed. Probably already gone' % task_id)
1466 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001467 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001468 return 0
1469
1470
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001471@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001472def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001473 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001474
1475 The result can be in multiple part if the execution was sharded. It can
1476 potentially have retries.
1477 """
1478 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001479 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001480 '-j',
1481 '--json',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001482 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001483 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001484 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001485 if not args and not options.json:
1486 parser.error('Must specify at least one task id or --json.')
1487 if args and options.json:
1488 parser.error('Only use one of task id or --json.')
1489
1490 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001491 options.json = six.text_type(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001492 try:
maruel1ceb3872015-10-14 06:10:44 -07001493 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001494 data = json.load(f)
1495 except (IOError, ValueError):
1496 parser.error('Failed to open %s' % options.json)
1497 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001498 tasks = sorted(data['tasks'].values(), key=lambda x: x['shard_index'])
maruel71c61c82016-02-22 06:52:05 -08001499 args = [t['task_id'] for t in tasks]
1500 except (KeyError, TypeError):
1501 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001502 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001503 # Take in account all the task slices.
1504 offset = 0
1505 for s in data['request']['task_slices']:
Junji Watanabecb054042020-07-21 08:43:26 +00001506 m = (
1507 offset + s['properties']['execution_timeout_secs'] +
1508 s['expiration_secs'])
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001509 if m > options.timeout:
1510 options.timeout = m
1511 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001512 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001513 else:
1514 valid = frozenset('0123456789abcdef')
1515 if any(not valid.issuperset(task_id) for task_id in args):
1516 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001517
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001518 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001519 return collect(options.swarming, args, options.timeout, options.decorate,
1520 options.print_status_updates, options.task_summary_json,
1521 options.task_output_dir, options.task_output_stdout,
1522 options.perf, options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001523 except Failure:
1524 on_error.report(None)
1525 return 1
1526
1527
maruel77f720b2015-09-15 12:35:22 -07001528@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001529def CMDpost(parser, args):
1530 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1531
1532 Input data must be sent to stdin, result is printed to stdout.
1533
1534 If HTTP response code >= 400, returns non-zero.
1535 """
1536 options, args = parser.parse_args(args)
1537 if len(args) != 1:
1538 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001539 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001540 data = sys.stdin.read()
1541 try:
1542 resp = net.url_read(url, data=data, method='POST')
1543 except net.TimeoutError:
1544 sys.stderr.write('Timeout!\n')
1545 return 1
1546 if not resp:
1547 sys.stderr.write('No response!\n')
1548 return 1
1549 sys.stdout.write(resp)
1550 return 0
1551
1552
1553@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001554def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001555 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1556 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001557
1558 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001559 Raw task request and results:
1560 swarming.py query -S server-url.com task/123456/request
1561 swarming.py query -S server-url.com task/123456/result
1562
maruel77f720b2015-09-15 12:35:22 -07001563 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001564 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001565
maruelaf6b06c2017-06-08 06:26:53 -07001566 Listing last 10 tasks on a specific bot named 'bot1':
1567 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001568
maruelaf6b06c2017-06-08 06:26:53 -07001569 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001570 quoting is important!:
1571 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001572 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001573 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001574 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001575 '-L',
1576 '--limit',
1577 type='int',
1578 default=200,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001579 help='Limit to enforce on limitless items (like number of tasks); '
Junji Watanabecb054042020-07-21 08:43:26 +00001580 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001581 parser.add_option(
1582 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001583 parser.add_option(
Junji Watanabecb054042020-07-21 08:43:26 +00001584 '--progress',
1585 action='store_true',
maruel77f720b2015-09-15 12:35:22 -07001586 help='Prints a dot at each request to show progress')
1587 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001588 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001589 parser.error(
1590 'Must specify only method name and optionally query args properly '
1591 'escaped.')
smut281c3902018-05-30 17:50:05 -07001592 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001593 try:
1594 data, yielder = get_yielder(base_url, options.limit)
1595 for items in yielder():
1596 if items:
1597 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001598 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001599 sys.stderr.write('.')
1600 sys.stderr.flush()
1601 except Failure as e:
1602 sys.stderr.write('\n%s\n' % e)
1603 return 1
maruel77f720b2015-09-15 12:35:22 -07001604 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001605 sys.stderr.write('\n')
1606 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001607 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001608 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001609 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001610 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001611 try:
maruel77f720b2015-09-15 12:35:22 -07001612 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001613 sys.stdout.write('\n')
1614 except IOError:
1615 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001616 return 0
1617
1618
maruel77f720b2015-09-15 12:35:22 -07001619def CMDquery_list(parser, args):
1620 """Returns list of all the Swarming APIs that can be used with command
1621 'query'.
1622 """
1623 parser.add_option(
1624 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1625 options, args = parser.parse_args(args)
1626 if args:
1627 parser.error('No argument allowed.')
1628
1629 try:
1630 apis = endpoints_api_discovery_apis(options.swarming)
1631 except APIError as e:
1632 parser.error(str(e))
1633 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001634 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001635 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001636 json.dump(apis, f)
1637 else:
1638 help_url = (
Junji Watanabecb054042020-07-21 08:43:26 +00001639 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1640 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001641 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -08001642 if i:
1643 print('')
Lei Leife202df2019-06-11 17:33:34 +00001644 print(api_id)
1645 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -08001646 if 'resources' in api:
1647 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001648 # TODO(maruel): Remove.
1649 # pylint: disable=too-many-nested-blocks
Junji Watanabecb054042020-07-21 08:43:26 +00001650 for j, (resource_name,
1651 resource) in enumerate(sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -08001652 if j:
1653 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001654 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -08001655 # Only list the GET ones.
1656 if method['httpMethod'] != 'GET':
1657 continue
Junji Watanabecb054042020-07-21 08:43:26 +00001658 print('- %s.%s: %s' % (resource_name, method_name, method['path']))
1659 print('\n'.join(' ' + l for l in textwrap.wrap(
1660 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +00001661 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -08001662 else:
1663 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001664 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -07001665 # Only list the GET ones.
1666 if method['httpMethod'] != 'GET':
1667 continue
Lei Leife202df2019-06-11 17:33:34 +00001668 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -08001669 print('\n'.join(
1670 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +00001671 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -07001672 return 0
1673
1674
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001675@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001676def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001677 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001678
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001679 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001680 """
1681 add_trigger_options(parser)
1682 add_collect_options(parser)
1683 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001684 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001685 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001686 try:
Ye Kuang3f3f2f72020-10-21 10:14:59 +00001687 tasks = trigger_task_shards(options.swarming, task_request)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001688 except Failure as e:
Junji Watanabecb054042020-07-21 08:43:26 +00001689 on_error.report('Failed to trigger %s(%s): %s' %
1690 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001691 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001692 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001693 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001694 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001695 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001696 task_ids = [
Junji Watanabe38b28b02020-04-23 10:23:30 +00001697 t['task_id']
1698 for t in sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001699 ]
Caleb Rouleau779c4f02019-05-22 21:18:49 +00001700 for task_id in task_ids:
1701 print('Task: {server}/task?id={task}'.format(
1702 server=options.swarming, task=task_id))
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001703 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001704 offset = 0
1705 for s in task_request.task_slices:
Junji Watanabecb054042020-07-21 08:43:26 +00001706 m = (offset + s.properties.execution_timeout_secs + s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001707 if m > options.timeout:
1708 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001709 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001710 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001711 try:
Junji Watanabecb054042020-07-21 08:43:26 +00001712 return collect(options.swarming, task_ids, options.timeout,
1713 options.decorate, options.print_status_updates,
1714 options.task_summary_json, options.task_output_dir,
1715 options.task_output_stdout, options.perf,
1716 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001717 except Failure:
1718 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001719 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001720
1721
maruel0eb1d1b2015-10-02 14:48:21 -07001722@subcommand.usage('bot_id')
1723def CMDterminate(parser, args):
1724 """Tells a bot to gracefully shut itself down as soon as it can.
1725
1726 This is done by completing whatever current task there is then exiting the bot
1727 process.
1728 """
1729 parser.add_option(
1730 '--wait', action='store_true', help='Wait for the bot to terminate')
1731 options, args = parser.parse_args(args)
1732 if len(args) != 1:
1733 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001734 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001735 request = net.url_read_json(url, data={})
1736 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001737 print('Failed to ask for termination', file=sys.stderr)
maruel0eb1d1b2015-10-02 14:48:21 -07001738 return 1
1739 if options.wait:
Junji Watanabecb054042020-07-21 08:43:26 +00001740 return collect(options.swarming, [request['task_id']], 0., False, False,
1741 None, None, [], False, None)
maruelbfc5f872017-06-10 16:43:17 -07001742 else:
Lei Leife202df2019-06-11 17:33:34 +00001743 print(request['task_id'])
maruel0eb1d1b2015-10-02 14:48:21 -07001744 return 0
1745
1746
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001747@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001748def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001749 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001750
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001751 Passes all extra arguments provided after '--' as additional command line
1752 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001753 """
1754 add_trigger_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001755 parser.add_option(
1756 '--dump-json',
1757 metavar='FILE',
1758 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001759 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001760 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001761 try:
Ye Kuang3f3f2f72020-10-21 10:14:59 +00001762 tasks = trigger_task_shards(options.swarming, task_request)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001763 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001764 print('Triggered task: %s' % task_request.name)
Junji Watanabecb054042020-07-21 08:43:26 +00001765 tasks_sorted = sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001766 if options.dump_json:
1767 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001768 'base_task_name': task_request.name,
1769 'tasks': tasks,
1770 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001771 }
maruel46b015f2015-10-13 18:40:35 -07001772 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001773 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001774 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001775 (options.swarming, options.dump_json))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001776 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001777 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001778 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Junji Watanabecb054042020-07-21 08:43:26 +00001779 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001780 print('Or visit:')
1781 for t in tasks_sorted:
1782 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001783 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001784 except Failure:
1785 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001786 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001787
1788
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001789class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +00001790
maruel@chromium.org0437a732013-08-27 16:05:52 +00001791 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001792 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001793 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001794 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001795 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001796 '-S',
1797 '--swarming',
1798 metavar='URL',
1799 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001800 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001801 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001802 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001803
1804 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001805 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001806 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001807 auth.process_auth_options(self, options)
1808 user = self._process_swarming(options)
1809 if hasattr(options, 'user') and not options.user:
1810 options.user = user
1811 return options, args
1812
1813 def _process_swarming(self, options):
1814 """Processes the --swarming option and aborts if not specified.
1815
1816 Returns the identity as determined by the server.
1817 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001818 if not options.swarming:
1819 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001820 try:
1821 options.swarming = net.fix_url(options.swarming)
1822 except ValueError as e:
1823 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +00001824
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001825 try:
1826 user = auth.ensure_logged_in(options.swarming)
1827 except ValueError as e:
1828 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001829 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001830
1831
1832def main(args):
1833 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001834 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001835
1836
1837if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001838 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001839 fix_encoding.fix_encoding()
1840 tools.disable_buffering()
1841 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +00001842 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001843 sys.exit(main(sys.argv[1:]))