blob: 6124583a15a3a0aa91ff5f29ab52306b8dc59152 [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
6"""Client tool to trigger tasks or retrieve results from a Swarming server."""
7
Lei Leife202df2019-06-11 17:33:34 +00008from __future__ import print_function
9
10__version__ = '1.0'
maruel@chromium.org0437a732013-08-27 16:05:52 +000011
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050012import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040013import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000014import json
15import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040016import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import os
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100018import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000019import sys
maruel11e31af2017-02-15 07:30:50 -080020import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070021import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000022import time
Takuto Ikuta35250172020-01-31 09:33:46 +000023import uuid
maruel@chromium.org0437a732013-08-27 16:05:52 +000024
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000025from utils import tools
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000026tools.force_local_third_party()
maruel@chromium.org0437a732013-08-27 16:05:52 +000027
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000028# third_party/
29import colorama
30from chromium import natsort
31from depot_tools import fix_encoding
32from depot_tools import subcommand
Takuto Ikuta6e2ff962019-10-29 12:35:27 +000033import six
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +000034from six.moves import urllib
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000035
36# pylint: disable=ungrouped-imports
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
iannucci31ab9192017-05-02 19:11:56 -070038import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +000040import isolate_storage
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040041import local_caching
maruelc070e672016-02-22 17:32:57 -080042import run_isolated
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000043from utils import file_path
44from utils import fs
45from utils import logging_utils
46from utils import net
47from utils import on_error
48from utils import subprocess42
49from utils import threading_utils
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050050
51
52class Failure(Exception):
53 """Generic failure."""
54 pass
55
56
maruel0a25f6c2017-05-10 10:43:23 -070057def default_task_name(options):
58 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050059 if not options.task_name:
Junji Watanabe38b28b02020-04-23 10:23:30 +000060 task_name = u'%s/%s' % (options.user, '_'.join(
61 '%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070062 if options.isolated:
63 task_name += u'/' + options.isolated
64 return task_name
65 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050066
67
68### Triggering.
69
70
maruel77f720b2015-09-15 12:35:22 -070071# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000072CipdPackage = collections.namedtuple('CipdPackage', [
73 'package_name',
74 'path',
75 'version',
76])
borenet02f772b2016-06-22 12:42:19 -070077
78
79# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000080CipdInput = collections.namedtuple('CipdInput', [
81 'client_package',
82 'packages',
83 'server',
84])
borenet02f772b2016-06-22 12:42:19 -070085
86# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +000087FilesRef = collections.namedtuple('FilesRef', [
88 'isolated',
89 'isolatedserver',
90 'namespace',
91])
maruel77f720b2015-09-15 12:35:22 -070092
93# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080094StringListPair = collections.namedtuple(
Junji Watanabe38b28b02020-04-23 10:23:30 +000095 'StringListPair',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000096 [
Junji Watanabe38b28b02020-04-23 10:23:30 +000097 'key',
98 'value', # repeated string
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +000099 ])
100
Junji Watanabe38b28b02020-04-23 10:23:30 +0000101# See ../appengine/swarming/swarming_rpcs.py.
102Containment = collections.namedtuple('Containment', [
103 'lower_priority',
104 'containment_type',
105])
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800106
107# See ../appengine/swarming/swarming_rpcs.py.
Junji Watanabe38b28b02020-04-23 10:23:30 +0000108TaskProperties = collections.namedtuple('TaskProperties', [
109 'caches',
110 'cipd_input',
111 'command',
112 'containment',
113 'relative_cwd',
114 'dimensions',
115 'env',
116 'env_prefixes',
117 'execution_timeout_secs',
118 'extra_args',
119 'grace_period_secs',
120 'idempotent',
121 'inputs_ref',
122 'io_timeout_secs',
123 'outputs',
124 'secret_bytes',
125])
maruel77f720b2015-09-15 12:35:22 -0700126
127
128# See ../appengine/swarming/swarming_rpcs.py.
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400129TaskSlice = collections.namedtuple(
130 'TaskSlice',
131 [
132 'expiration_secs',
133 'properties',
134 'wait_for_capacity',
135 ])
136
137
138# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700139NewTaskRequest = collections.namedtuple(
140 'NewTaskRequest',
141 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500142 'name',
maruel77f720b2015-09-15 12:35:22 -0700143 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400145 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700146 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500147 'tags',
148 'user',
Robert Iannuccifafa7352018-06-13 17:08:17 +0000149 'pool_task_template',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500150 ])
151
152
maruel77f720b2015-09-15 12:35:22 -0700153def namedtuple_to_dict(value):
154 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400155 if hasattr(value, '_asdict'):
156 return namedtuple_to_dict(value._asdict())
157 if isinstance(value, (list, tuple)):
158 return [namedtuple_to_dict(v) for v in value]
159 if isinstance(value, dict):
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000160 return {k: namedtuple_to_dict(v) for k, v in value.items()}
Lei Lei73a5f732020-03-23 20:36:14 +0000161 # json.dumps in Python3 doesn't support bytes.
162 if isinstance(value, bytes):
163 return six.ensure_str(value)
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400164 return value
maruel77f720b2015-09-15 12:35:22 -0700165
166
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700167def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800168 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700169
170 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 """
maruel77f720b2015-09-15 12:35:22 -0700172 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700173 # Don't send 'service_account' if it is None to avoid confusing older
174 # version of the server that doesn't know about 'service_account' and don't
175 # use it at all.
176 if not out['service_account']:
177 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000178 for task_slice in out['task_slices']:
179 task_slice['properties']['env'] = [
180 {'key': k, 'value': v}
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000181 for k, v in task_slice['properties']['env'].items()
Brad Hallf78187a2018-10-19 17:08:55 +0000182 ]
183 task_slice['properties']['env'].sort(key=lambda x: x['key'])
Takuto Ikuta35250172020-01-31 09:33:46 +0000184 out['request_uuid'] = str(uuid.uuid4())
maruel77f720b2015-09-15 12:35:22 -0700185 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500186
187
maruel77f720b2015-09-15 12:35:22 -0700188def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500189 """Triggers a request on the Swarming server and returns the json data.
190
191 It's the low-level function.
192
193 Returns:
194 {
195 'request': {
196 'created_ts': u'2010-01-02 03:04:05',
197 'name': ..
198 },
199 'task_id': '12300',
200 }
201 """
202 logging.info('Triggering: %s', raw_request['name'])
203
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500204 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700205 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500206 if not result:
207 on_error.report('Failed to trigger task %s' % raw_request['name'])
208 return None
maruele557bce2015-11-17 09:01:27 -0800209 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800210 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800211 msg = 'Failed to trigger task %s' % raw_request['name']
212 if result['error'].get('errors'):
213 for err in result['error']['errors']:
214 if err.get('message'):
215 msg += '\nMessage: %s' % err['message']
216 if err.get('debugInfo'):
217 msg += '\nDebug info:\n%s' % err['debugInfo']
218 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800219 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800220
221 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800222 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500223 return result
224
225
226def setup_googletest(env, shards, index):
227 """Sets googletest specific environment variables."""
228 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700229 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
230 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
231 env = env[:]
232 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
233 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500234 return env
235
236
237def trigger_task_shards(swarming, task_request, shards):
238 """Triggers one or many subtasks of a sharded task.
239
240 Returns:
241 Dict with task details, returned to caller as part of --dump-json output.
242 None in case of failure.
243 """
244 def convert(index):
Erik Chend50a88f2019-02-16 01:22:07 +0000245 """
246 Args:
247 index: The index of the task request.
248
249 Returns:
250 raw_request: A swarming compatible JSON dictionary of the request.
251 shard_index: The index of the shard, which may be different than the index
252 of the task request.
253 """
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700254 req = task_request_to_raw_request(task_request)
Erik Chend50a88f2019-02-16 01:22:07 +0000255 shard_index = index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500256 if shards > 1:
Brad Hall157bec82018-11-26 22:15:38 +0000257 for task_slice in req['task_slices']:
258 task_slice['properties']['env'] = setup_googletest(
259 task_slice['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700260 req['name'] += ':%s:%s' % (index, shards)
Erik Chend50a88f2019-02-16 01:22:07 +0000261 else:
262 task_slices = req['task_slices']
263
Lei Lei73a5f732020-03-23 20:36:14 +0000264 total_shards = 1
Erik Chend50a88f2019-02-16 01:22:07 +0000265 # Multiple tasks slices might exist if there are optional "slices", e.g.
266 # multiple ways of dispatching the task that should be equivalent. These
267 # should be functionally equivalent but we have cannot guarantee that. If
268 # we see the GTEST_SHARD_INDEX env var, we assume that it applies to all
269 # slices.
270 for task_slice in task_slices:
271 for env_var in task_slice['properties']['env']:
272 if env_var['key'] == 'GTEST_SHARD_INDEX':
273 shard_index = int(env_var['value'])
274 if env_var['key'] == 'GTEST_TOTAL_SHARDS':
275 total_shards = int(env_var['value'])
276 if total_shards > 1:
277 req['name'] += ':%s:%s' % (shard_index, total_shards)
Ben Pastened2a7be42020-07-14 22:28:55 +0000278 if shard_index and total_shards:
279 req['tags'] += [
280 'shard_index:%d' % shard_index,
281 'total_shards:%d' % total_shards,
282 ]
Erik Chend50a88f2019-02-16 01:22:07 +0000283
284 return req, shard_index
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500285
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000286 requests = [convert(index) for index in range(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500287 tasks = {}
288 priority_warning = False
Erik Chend50a88f2019-02-16 01:22:07 +0000289 for request, shard_index in requests:
maruel77f720b2015-09-15 12:35:22 -0700290 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500291 if not task:
292 break
293 logging.info('Request result: %s', task)
294 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400295 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500296 priority_warning = True
Lei Leife202df2019-06-11 17:33:34 +0000297 print('Priority was reset to %s' % task['request']['priority'],
298 file=sys.stderr)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500299 tasks[request['name']] = {
Erik Chend50a88f2019-02-16 01:22:07 +0000300 'shard_index': shard_index,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500301 'task_id': task['task_id'],
302 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
303 }
304
305 # Some shards weren't triggered. Abort everything.
306 if len(tasks) != len(requests):
307 if tasks:
Lei Leife202df2019-06-11 17:33:34 +0000308 print('Only %d shard(s) out of %d were triggered' % (
309 len(tasks), len(requests)), file=sys.stderr)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000310 for task_dict in tasks.values():
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500311 abort_task(swarming, task_dict['task_id'])
312 return None
313
314 return tasks
315
316
317### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000318
319
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700320# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000321STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700322
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400323
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000324class TaskState(object):
325 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000326
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000327 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
328 is the source of truth for these values:
329 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400330
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000331 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400332 """
333 RUNNING = 0x10
334 PENDING = 0x20
335 EXPIRED = 0x30
336 TIMED_OUT = 0x40
337 BOT_DIED = 0x50
338 CANCELED = 0x60
339 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400340 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400341 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400342
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000343 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400344
maruel77f720b2015-09-15 12:35:22 -0700345 _ENUMS = {
346 'RUNNING': RUNNING,
347 'PENDING': PENDING,
348 'EXPIRED': EXPIRED,
349 'TIMED_OUT': TIMED_OUT,
350 'BOT_DIED': BOT_DIED,
351 'CANCELED': CANCELED,
352 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400353 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400354 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700355 }
356
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400357 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700358 def from_enum(cls, state):
359 """Returns int value based on the string."""
360 if state not in cls._ENUMS:
361 raise ValueError('Invalid state %s' % state)
362 return cls._ENUMS[state]
363
maruel@chromium.org0437a732013-08-27 16:05:52 +0000364
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700365class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700366 """Assembles task execution summary (for --task-summary-json output).
367
368 Optionally fetches task outputs from isolate server to local disk (used when
369 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370
371 This object is shared among multiple threads running 'retrieve_results'
372 function, in particular they call 'process_shard_result' method in parallel.
373 """
374
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000375 def __init__(self, task_output_dir, task_output_stdout, shard_count,
376 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
378
379 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700380 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700381 shard_count: expected number of task shards.
382 """
maruel12e30012015-10-09 11:55:35 -0700383 self.task_output_dir = (
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000384 six.text_type(os.path.abspath(task_output_dir))
maruel12e30012015-10-09 11:55:35 -0700385 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000386 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000388 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700389
390 self._lock = threading.Lock()
391 self._per_shard_results = {}
392 self._storage = None
393
nodire5028a92016-04-29 14:38:21 -0700394 if self.task_output_dir:
395 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700396
Vadim Shtayurab450c602014-05-12 19:23:25 -0700397 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700398 """Stores results of a single task shard, fetches output files if necessary.
399
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400400 Modifies |result| in place.
401
maruel77f720b2015-09-15 12:35:22 -0700402 shard_index is 0-based.
403
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700404 Called concurrently from multiple threads.
405 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700406 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700407 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700408 if shard_index < 0 or shard_index >= self.shard_count:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000409 logging.warning('Shard index %d is outside of expected range: [0; %d]',
410 shard_index, self.shard_count - 1)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700411 return
412
maruel77f720b2015-09-15 12:35:22 -0700413 if result.get('outputs_ref'):
414 ref = result['outputs_ref']
415 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
416 ref['isolatedserver'],
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000417 urllib.parse.urlencode([('namespace', ref['namespace']),
418 ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400419
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700420 # Store result dict of that shard, ignore results we've already seen.
421 with self._lock:
422 if shard_index in self._per_shard_results:
423 logging.warning('Ignoring duplicate shard index %d', shard_index)
424 return
425 self._per_shard_results[shard_index] = result
426
427 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700428 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000429 server_ref = isolate_storage.ServerRef(
430 result['outputs_ref']['isolatedserver'],
431 result['outputs_ref']['namespace'])
432 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400433 if storage:
434 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400435 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
436 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400437 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700438 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400439 storage,
Lei Leife202df2019-06-11 17:33:34 +0000440 local_caching.MemoryContentAddressedCache(file_mode_mask=0o700),
maruel4409e302016-07-19 14:25:51 -0700441 os.path.join(self.task_output_dir, str(shard_index)),
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000442 False, self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443
444 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700445 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700446 with self._lock:
447 # Write an array of shard results with None for missing shards.
448 summary = {
Marc-Antoine Ruel0fdee222019-10-10 14:42:40 +0000449 'shards': [
450 self._per_shard_results.get(i) for i in range(self.shard_count)
451 ],
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000453
454 # Don't store stdout in the summary if not requested too.
455 if "json" not in self.task_output_stdout:
456 for shard_json in summary['shards']:
457 if not shard_json:
458 continue
459 if "output" in shard_json:
460 del shard_json["output"]
461 if "outputs" in shard_json:
462 del shard_json["outputs"]
463
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700464 # Write summary.json to task_output_dir as well.
465 if self.task_output_dir:
466 tools.write_json(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000467 os.path.join(self.task_output_dir, u'summary.json'), summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700468 if self._storage:
469 self._storage.close()
470 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700471 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700472
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000473 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700474 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700475 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700476 with self._lock:
477 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000478 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700479 else:
480 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000481 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700482 logging.error(
483 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000484 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700485 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000486 if self._storage.server_ref.namespace != server_ref.namespace:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000487 logging.error('Task shards are using multiple namespaces: %s and %s',
488 self._storage.server_ref.namespace,
489 server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700490 return None
491 return self._storage
492
493
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500494def now():
495 """Exists so it can be mocked easily."""
496 return time.time()
497
498
maruel77f720b2015-09-15 12:35:22 -0700499def parse_time(value):
500 """Converts serialized time from the API to datetime.datetime."""
501 # When microseconds are 0, the '.123456' suffix is elided. This means the
502 # serialized format is not consistent, which confuses the hell out of python.
503 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
504 try:
505 return datetime.datetime.strptime(value, fmt)
506 except ValueError:
507 pass
508 raise ValueError('Failed to parse %s' % value)
509
510
Junji Watanabe38b28b02020-04-23 10:23:30 +0000511def retrieve_results(base_url, shard_index, task_id, timeout, should_stop,
512 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400513 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700514
Vadim Shtayurab450c602014-05-12 19:23:25 -0700515 Returns:
516 <result dict> on success.
517 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700518 """
maruel71c61c82016-02-22 06:52:05 -0800519 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700520 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700521 if include_perf:
522 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700523 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700524 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400525 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700526 attempt = 0
527
528 while not should_stop.is_set():
529 attempt += 1
530
531 # Waiting for too long -> give up.
532 current_time = now()
533 if deadline and current_time >= deadline:
534 logging.error('retrieve_results(%s) timed out on attempt %d',
535 base_url, attempt)
536 return None
537
538 # Do not spin too fast. Spin faster at the beginning though.
539 # Start with 1 sec delay and for each 30 sec of waiting add another second
540 # of delay, until hitting 15 sec ceiling.
541 if attempt > 1:
542 max_delay = min(15, 1 + (current_time - started) / 30.0)
543 delay = min(max_delay, deadline - current_time) if deadline else max_delay
544 if delay > 0:
545 logging.debug('Waiting %.1f sec before retrying', delay)
546 should_stop.wait(delay)
547 if should_stop.is_set():
548 return None
549
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400550 # Disable internal retries in net.url_read_json, since we are doing retries
551 # ourselves.
552 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700553 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
554 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400555 # Retry on 500s only if no timeout is specified.
556 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400557 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400558 if timeout == -1:
559 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400560 continue
maruel77f720b2015-09-15 12:35:22 -0700561
maruelbf53e042015-12-01 15:00:51 -0800562 if result.get('error'):
563 # An error occurred.
564 if result['error'].get('errors'):
565 for err in result['error']['errors']:
Junji Watanabe38b28b02020-04-23 10:23:30 +0000566 logging.warning('Error while reading task: %s; %s',
567 err.get('message'), err.get('debugInfo'))
maruelbf53e042015-12-01 15:00:51 -0800568 elif result['error'].get('message'):
569 logging.warning(
570 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400571 if timeout == -1:
572 return result
maruelbf53e042015-12-01 15:00:51 -0800573 continue
574
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400575 # When timeout == -1, always return on first attempt. 500s are already
576 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000577 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000578 if fetch_stdout:
579 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700580 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700581 # Record the result, try to fetch attached output files (if any).
582 if output_collector:
583 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700584 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700585 if result.get('internal_failure'):
586 logging.error('Internal error!')
587 elif result['state'] == 'BOT_DIED':
588 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700589 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000590
591
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700592def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400593 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000594 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500595 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000596
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700597 Duplicate shards are ignored. Shards are yielded in order of completion.
598 Timed out shards are NOT yielded at all. Caller can compare number of yielded
599 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000600
601 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500602 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 +0000603 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500604
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700605 output_collector is an optional instance of TaskOutputCollector that will be
606 used to fetch files produced by a task from isolate server to the local disk.
607
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500608 Yields:
609 (index, result). In particular, 'result' is defined as the
610 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000611 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000612 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400613 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700614 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700615 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700616
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
618 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700619 # Adds a task to the thread pool to call 'retrieve_results' and return
620 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400621 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000622 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700623 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400625 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000626 task_id, timeout, should_stop, output_collector, include_perf,
627 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700628
629 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400630 for shard_index, task_id in enumerate(task_ids):
631 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700632
633 # Wait for all of them to finish.
Lei Lei73a5f732020-03-23 20:36:14 +0000634 # Convert to list, since range in Python3 doesn't have remove.
635 shards_remaining = list(range(len(task_ids)))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400636 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700637 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700638 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700639 try:
Marc-Antoine Ruel4494b6c2018-11-28 21:00:41 +0000640 shard_index, result = results_channel.next(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700641 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700642 except threading_utils.TaskChannel.Timeout:
643 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000644 time_now = str(datetime.datetime.now())
645 _, time_now = time_now.split(' ')
Junji Watanabe38b28b02020-04-23 10:23:30 +0000646 print('%s '
647 'Waiting for results from the following shards: %s' %
648 (time_now, ', '.join(map(str, shards_remaining))))
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 sys.stdout.flush()
650 continue
651 except Exception:
652 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653
654 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700655 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500657 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000658 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700659
Vadim Shtayurab450c602014-05-12 19:23:25 -0700660 # Yield back results to the caller.
661 assert shard_index in shards_remaining
662 shards_remaining.remove(shard_index)
663 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700664
maruel@chromium.org0437a732013-08-27 16:05:52 +0000665 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700666 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000667 should_stop.set()
668
669
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000670def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000671 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700672 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400673 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700674 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
675 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400676 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
677 metadata.get('abandoned_ts')):
678 pending = '%.1fs' % (
679 parse_time(metadata['abandoned_ts']) -
680 parse_time(metadata['created_ts'])
681 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400682 else:
683 pending = 'N/A'
684
maruel77f720b2015-09-15 12:35:22 -0700685 if metadata.get('duration') is not None:
686 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400687 else:
688 duration = 'N/A'
689
maruel77f720b2015-09-15 12:35:22 -0700690 if metadata.get('exit_code') is not None:
691 # Integers are encoded as string to not loose precision.
692 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400693 else:
694 exit_code = 'N/A'
695
696 bot_id = metadata.get('bot_id') or 'N/A'
697
maruel77f720b2015-09-15 12:35:22 -0700698 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400699 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000700 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400701 if metadata.get('state') == 'CANCELED':
702 tag_footer2 = ' Pending: %s CANCELED' % pending
703 elif metadata.get('state') == 'EXPIRED':
704 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400705 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400706 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
707 pending, duration, bot_id, exit_code, metadata['state'])
708 else:
709 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
710 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400711
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000712 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
713 dash_pad = '+-%s-+' % ('-' * tag_len)
714 tag_header = '| %s |' % tag_header.ljust(tag_len)
715 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
716 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400717
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000718 if include_stdout:
719 return '\n'.join([
720 dash_pad,
721 tag_header,
722 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400723 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000724 dash_pad,
725 tag_footer1,
726 tag_footer2,
727 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000728 ])
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000729 return '\n'.join([
730 dash_pad,
731 tag_header,
732 tag_footer2,
733 dash_pad,
Junji Watanabe38b28b02020-04-23 10:23:30 +0000734 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000735
736
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700737def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700738 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000739 task_summary_json, task_output_dir, task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000740 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700741 """Retrieves results of a Swarming task.
742
743 Returns:
744 process exit code that should be returned to the user.
745 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000746
747 filter_cb = None
748 if filepath_filter:
749 filter_cb = re.compile(filepath_filter).match
750
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700751 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000752 output_collector = TaskOutputCollector(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000753 task_output_dir, task_output_stdout, len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700754
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700755 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700756 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400757 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700758 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400759 for index, metadata in yield_results(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000760 swarming,
761 task_ids,
762 timeout,
763 None,
764 print_status_updates,
765 output_collector,
766 include_perf,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000767 (len(task_output_stdout) > 0),
Junji Watanabe38b28b02020-04-23 10:23:30 +0000768 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700770
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400771 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700772 shard_exit_code = metadata.get('exit_code')
773 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700774 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700775 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700776 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400777 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700778 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700779
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700780 if decorate:
Lei Lei73a5f732020-03-23 20:36:14 +0000781 # s is bytes in Python3, print could not print
782 # s with nice format, so decode s to str.
783 s = six.ensure_str(
784 decorate_shard_output(swarming, index, metadata,
785 "console" in task_output_stdout).encode(
786 'utf-8', 'replace'))
leileied181762016-10-13 14:24:59 -0700787 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400788 if len(seen_shards) < len(task_ids):
789 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790 else:
maruel77f720b2015-09-15 12:35:22 -0700791 print('%s: %s %s' % (
792 metadata.get('bot_id', 'N/A'),
793 metadata['task_id'],
794 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000795 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700796 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400797 if output:
798 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700799 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700800 summary = output_collector.finalize()
801 if task_summary_json:
802 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700803
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400804 if decorate and total_duration:
805 print('Total duration: %.1fs' % total_duration)
806
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400807 if len(seen_shards) != len(task_ids):
808 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Lei Leife202df2019-06-11 17:33:34 +0000809 print('Results from some shards are missing: %s' %
810 ', '.join(map(str, missing_shards)), file=sys.stderr)
Vadim Shtayurac524f512014-05-15 09:54:56 -0700811 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700812
maruela5490782015-09-30 10:56:59 -0700813 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000814
815
maruel77f720b2015-09-15 12:35:22 -0700816### API management.
817
818
819class APIError(Exception):
820 pass
821
822
823def endpoints_api_discovery_apis(host):
824 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
825 the APIs exposed by a host.
826
827 https://developers.google.com/discovery/v1/reference/apis/list
828 """
maruel380e3262016-08-31 16:10:06 -0700829 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
830 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700831 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
832 if data is None:
833 raise APIError('Failed to discover APIs on %s' % host)
834 out = {}
835 for api in data['items']:
836 if api['id'] == 'discovery:v1':
837 continue
838 # URL is of the following form:
839 # url = host + (
840 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
841 api_data = net.url_read_json(api['discoveryRestUrl'])
842 if api_data is None:
843 raise APIError('Failed to discover %s on %s' % (api['id'], host))
844 out[api['id']] = api_data
845 return out
846
847
maruelaf6b06c2017-06-08 06:26:53 -0700848def get_yielder(base_url, limit):
849 """Returns the first query and a function that yields following items."""
850 CHUNK_SIZE = 250
851
852 url = base_url
853 if limit:
854 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
855 data = net.url_read_json(url)
856 if data is None:
857 # TODO(maruel): Do basic diagnostic.
858 raise Failure('Failed to access %s' % url)
859 org_cursor = data.pop('cursor', None)
860 org_total = len(data.get('items') or [])
861 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
862 if not org_cursor or not org_total:
863 # This is not an iterable resource.
864 return data, lambda: []
865
866 def yielder():
867 cursor = org_cursor
868 total = org_total
869 # Some items support cursors. Try to get automatically if cursors are needed
870 # by looking at the 'cursor' items.
871 while cursor and (not limit or total < limit):
872 merge_char = '&' if '?' in base_url else '?'
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +0000873 url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor))
maruelaf6b06c2017-06-08 06:26:53 -0700874 if limit:
875 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
876 new = net.url_read_json(url)
877 if new is None:
878 raise Failure('Failed to access %s' % url)
879 cursor = new.get('cursor')
880 new_items = new.get('items')
881 nb_items = len(new_items or [])
882 total += nb_items
883 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
884 yield new_items
885
886 return data, yielder
887
888
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500889### Commands.
890
891
892def abort_task(_swarming, _manifest):
893 """Given a task manifest that was triggered, aborts its execution."""
894 # TODO(vadimsh): No supported by the server yet.
895
896
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400897def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800898 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500899 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500900 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500901 dest='dimensions', metavar='FOO bar',
902 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000903 parser.filter_group.add_option(
904 '--optional-dimension', default=[], action='append', nargs=3,
905 dest='optional_dimensions', metavar='key value expiration',
906 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500907 parser.add_option_group(parser.filter_group)
908
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909
Brad Hallf78187a2018-10-19 17:08:55 +0000910def _validate_filter_option(parser, key, value, expiration, argname):
911 if ':' in key:
912 parser.error('%s key cannot contain ":"' % argname)
913 if key.strip() != key:
914 parser.error('%s key has whitespace' % argname)
915 if not key:
916 parser.error('%s key is empty' % argname)
917
918 if value.strip() != value:
919 parser.error('%s value has whitespace' % argname)
920 if not value:
921 parser.error('%s value is empty' % argname)
922
923 if expiration is not None:
924 try:
925 expiration = int(expiration)
926 except ValueError:
927 parser.error('%s expiration is not an integer' % argname)
928 if expiration <= 0:
929 parser.error('%s expiration should be positive' % argname)
930 if expiration % 60 != 0:
931 parser.error('%s expiration is not divisible by 60' % argname)
932
933
maruelaf6b06c2017-06-08 06:26:53 -0700934def process_filter_options(parser, options):
935 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000936 _validate_filter_option(parser, key, value, None, 'dimension')
937 for key, value, exp in options.optional_dimensions:
938 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700939 options.dimensions.sort()
940
941
Vadim Shtayurab450c602014-05-12 19:23:25 -0700942def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400943 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700944 parser.sharding_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000945 '--shards',
946 type='int',
947 default=1,
948 metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700949 help='Number of shards to trigger and collect.')
950 parser.add_option_group(parser.sharding_group)
951
952
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400953def add_trigger_options(parser):
954 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500955 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400956 add_filter_options(parser)
957
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400958 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800959 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000960 '-s',
961 '--isolated',
962 metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500963 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800964 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000965 '-e',
966 '--env',
967 default=[],
968 action='append',
969 nargs=2,
970 metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700971 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800972 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000973 '--env-prefix',
974 default=[],
975 action='append',
976 nargs=2,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800977 metavar='VAR local/path',
978 help='Prepend task-relative `local/path` to the task\'s VAR environment '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000979 'variable using os-appropriate pathsep character. Can be specified '
980 'multiple times for the same VAR to add multiple paths.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800981 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000982 '--idempotent',
983 action='store_true',
984 default=False,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400985 help='When set, the server will actively try to find a previous task '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000986 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800987 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000988 '--secret-bytes-path',
989 metavar='FILE',
Stephen Martinisf391c772019-02-01 01:22:12 +0000990 help='The optional path to a file containing the secret_bytes to use '
Junji Watanabe38b28b02020-04-23 10:23:30 +0000991 'with this task.')
maruel681d6802017-01-17 16:56:03 -0800992 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000993 '--hard-timeout',
994 type='int',
995 default=60 * 60,
996 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400997 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800998 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000999 '--io-timeout',
1000 type='int',
1001 default=20 * 60,
1002 metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001003 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001004 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001005 '--lower-priority',
1006 action='store_true',
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001007 help='Lowers the child process priority')
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001008 containment_choices = ('NONE', 'AUTO', 'JOB_OBJECT')
1009 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001010 '--containment-type',
1011 default='NONE',
1012 metavar='NONE',
Marc-Antoine Ruel7f61a4d2019-05-22 20:10:07 +00001013 choices=containment_choices,
1014 help='Containment to use; one of: %s' % ', '.join(containment_choices))
maruel681d6802017-01-17 16:56:03 -08001015 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001016 '--raw-cmd',
1017 action='store_true',
1018 default=False,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001019 help='When set, the command after -- is used as-is without run_isolated. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001020 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -08001021 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001022 '--relative-cwd',
1023 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001024 'requires --raw-cmd')
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001025 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001026 '--cipd-package',
1027 action='append',
1028 default=[],
1029 metavar='PKG',
maruel5475ba62017-05-31 15:35:47 -07001030 help='CIPD packages to install on the Swarming bot. Uses the format: '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001031 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001032 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001033 '--named-cache',
1034 action='append',
1035 nargs=2,
1036 default=[],
maruel5475ba62017-05-31 15:35:47 -07001037 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001038 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1039 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001040 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001041 help='Email of a service account to run the task as, or literal "bot" '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001042 'string to indicate that the task should use the same account the '
1043 'bot itself is using to authenticate to Swarming. Don\'t use task '
1044 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001045 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001046 '--pool-task-template',
1047 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1048 default='AUTO',
1049 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001050 'By default, the pool\'s TaskTemplate is automatically selected, '
1051 'according the pool configuration on the server. Choices are: '
1052 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
Robert Iannuccifafa7352018-06-13 17:08:17 +00001053 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001054 '-o',
1055 '--output',
1056 action='append',
1057 default=[],
1058 metavar='PATH',
maruel5475ba62017-05-31 15:35:47 -07001059 help='A list of files to return in addition to those written to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001060 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1061 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001062 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001063 '--wait-for-capacity',
1064 action='store_true',
1065 default=False,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001066 help='Instructs to leave the task PENDING even if there\'s no known bot '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001067 'that could run this task, otherwise the task will be denied with '
1068 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001069 parser.add_option_group(group)
1070
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001071 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001072 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001073 '--priority',
1074 type='int',
1075 default=200,
maruel681d6802017-01-17 16:56:03 -08001076 help='The lower value, the more important the task is')
1077 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001078 '-T',
1079 '--task-name',
1080 metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001081 help='Display name of the task. Defaults to '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001082 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1083 'isolated file is provided, if a hash is provided, it defaults to '
1084 '<user>/<dimensions>/<isolated hash>/<timestamp>')
maruel681d6802017-01-17 16:56:03 -08001085 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001086 '--tags',
1087 action='append',
1088 default=[],
1089 metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001090 help='Tags to assign to the task.')
1091 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001092 '--user',
1093 default='',
maruel681d6802017-01-17 16:56:03 -08001094 help='User associated with the task. Defaults to authenticated user on '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001095 'the server.')
maruel681d6802017-01-17 16:56:03 -08001096 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001097 '--expiration',
1098 type='int',
1099 default=6 * 60 * 60,
1100 metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001101 help='Seconds to allow the task to be pending for a bot to run before '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001102 'this task request expires.')
maruel681d6802017-01-17 16:56:03 -08001103 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001104 '--deadline', type='int', dest='expiration', help=optparse.SUPPRESS_HELP)
maruel681d6802017-01-17 16:56:03 -08001105 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001106
1107
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001108def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001109 """Processes trigger options and does preparatory steps.
1110
1111 Returns:
1112 NewTaskRequest instance.
1113 """
maruelaf6b06c2017-06-08 06:26:53 -07001114 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001115 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001116 if args and args[0] == '--':
1117 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001118
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001119 if not options.dimensions:
1120 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001121 if not any(k == 'pool' for k, _v in options.dimensions):
1122 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001123 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1124 parser.error('--tags must be in the format key:value')
1125 if options.raw_cmd and not args:
1126 parser.error(
1127 'Arguments with --raw-cmd should be passed after -- as command '
1128 'delimiter.')
1129 if options.isolate_server and not options.namespace:
1130 parser.error(
1131 '--namespace must be a valid value when --isolate-server is used')
1132 if not options.isolated and not options.raw_cmd:
1133 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1134
1135 # Isolated
1136 # --isolated is required only if --raw-cmd wasn't provided.
1137 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1138 # preferred server.
Takuto Ikutaae767b32020-05-11 01:22:19 +00001139 isolateserver.process_isolate_server_options(parser, options,
1140 not options.raw_cmd)
maruel0a25f6c2017-05-10 10:43:23 -07001141 inputs_ref = None
1142 if options.isolate_server:
1143 inputs_ref = FilesRef(
1144 isolated=options.isolated,
1145 isolatedserver=options.isolate_server,
1146 namespace=options.namespace)
1147
1148 # Command
1149 command = None
1150 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001151 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001152 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001153 if options.relative_cwd:
1154 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1155 if not a.startswith(os.getcwd()):
1156 parser.error(
1157 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001158 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001159 if options.relative_cwd:
1160 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001161 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001162
maruel0a25f6c2017-05-10 10:43:23 -07001163 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001164 cipd_packages = []
1165 for p in options.cipd_package:
1166 split = p.split(':', 2)
1167 if len(split) != 3:
1168 parser.error('CIPD packages must take the form: path:package:version')
Junji Watanabe38b28b02020-04-23 10:23:30 +00001169 cipd_packages.append(
1170 CipdPackage(package_name=split[1], path=split[0], version=split[2]))
borenet02f772b2016-06-22 12:42:19 -07001171 cipd_input = None
1172 if cipd_packages:
1173 cipd_input = CipdInput(
1174 client_package=None,
1175 packages=cipd_packages,
1176 server=None)
1177
maruel0a25f6c2017-05-10 10:43:23 -07001178 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001179 secret_bytes = None
1180 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001181 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001182 secret_bytes = f.read().encode('base64')
1183
maruel0a25f6c2017-05-10 10:43:23 -07001184 # Named caches
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001185 caches = [{
1186 u'name': six.text_type(i[0]),
1187 u'path': six.text_type(i[1])
1188 } for i in options.named_cache]
maruel0a25f6c2017-05-10 10:43:23 -07001189
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001190 env_prefixes = {}
1191 for k, v in options.env_prefix:
1192 env_prefixes.setdefault(k, []).append(v)
1193
Brad Hallf78187a2018-10-19 17:08:55 +00001194 # Get dimensions into the key/value format we can manipulate later.
1195 orig_dims = [
1196 {'key': key, 'value': value} for key, value in options.dimensions]
1197 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1198
1199 # Construct base properties that we will use for all the slices, adding in
1200 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001201 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001202 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001203 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001204 command=command,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001205 containment=Containment(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001206 lower_priority=bool(options.lower_priority),
1207 containment_type=options.containment_type,
Marc-Antoine Ruel89669dc2019-05-01 14:01:08 +00001208 ),
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001209 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001210 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001211 env=options.env,
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001212 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.items()],
maruel77f720b2015-09-15 12:35:22 -07001213 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001214 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001215 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001216 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001217 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001218 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001219 outputs=options.output,
1220 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001221
1222 slices = []
1223
1224 # Group the optional dimensions by expiration.
1225 dims_by_exp = {}
1226 for key, value, exp_secs in options.optional_dimensions:
1227 dims_by_exp.setdefault(int(exp_secs), []).append(
1228 {'key': key, 'value': value})
1229
1230 # Create the optional slices with expiration deltas, we fix up the properties
1231 # below.
1232 last_exp = 0
1233 for expiration_secs in sorted(dims_by_exp):
1234 t = TaskSlice(
1235 expiration_secs=expiration_secs - last_exp,
1236 properties=properties,
1237 wait_for_capacity=False)
1238 slices.append(t)
1239 last_exp = expiration_secs
1240
1241 # Add back in the default slice (the last one).
1242 exp = max(int(options.expiration) - last_exp, 60)
1243 base_task_slice = TaskSlice(
1244 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001245 properties=properties,
1246 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001247 slices.append(base_task_slice)
1248
Brad Hall7f463e62018-11-16 16:13:30 +00001249 # Add optional dimensions to the task slices, replacing a dimension that
1250 # has the same key if it is a dimension where repeating isn't valid (otherwise
1251 # we append it). Currently the only dimension we can repeat is "caches"; the
1252 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001253 extra_dims = []
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001254 for i, (_, kvs) in enumerate(sorted(dims_by_exp.items(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001255 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001256 # Replace or append the key/value pairs for this expiration in extra_dims;
1257 # we keep extra_dims around because we are iterating backwards and filling
1258 # in slices with shorter expirations. Dimensions expire as time goes on so
1259 # the slices that expire earlier will generally have more dimensions.
1260 for kv in kvs:
1261 if kv['key'] == 'caches':
1262 extra_dims.append(kv)
1263 else:
1264 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1265 # Then, add all the optional dimensions to the original dimension set, again
1266 # replacing if needed.
1267 for kv in extra_dims:
1268 if kv['key'] == 'caches':
1269 dims.append(kv)
1270 else:
1271 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001272 dims.sort(key=lambda x: (x['key'], x['value']))
1273 slice_properties = properties._replace(dimensions=dims)
1274 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1275
maruel77f720b2015-09-15 12:35:22 -07001276 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001277 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001278 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001279 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001280 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001281 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001282 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001283 user=options.user,
1284 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001285
1286
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001287class TaskOutputStdoutOption(optparse.Option):
1288 """Where to output the each task's console output (stderr/stdout).
1289
1290 The output will be;
1291 none - not be downloaded.
1292 json - stored in summary.json file *only*.
1293 console - shown on stdout *only*.
1294 all - stored in summary.json and shown on stdout.
1295 """
1296
1297 choices = ['all', 'json', 'console', 'none']
1298
1299 def __init__(self, *args, **kw):
1300 optparse.Option.__init__(
1301 self,
1302 *args,
1303 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001304 default=['console', 'json'],
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001305 help=re.sub(r'\s\s*', ' ', self.__doc__),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001306 **kw)
1307
1308 def convert_value(self, opt, value):
1309 if value not in self.choices:
1310 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1311 self.get_opt_string(), self.choices, value))
1312 stdout_to = []
1313 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001314 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001315 elif value != 'none':
1316 stdout_to = [value]
1317 return stdout_to
1318
1319
maruel@chromium.org0437a732013-08-27 16:05:52 +00001320def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001321 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001322 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001323 help='Timeout to wait for result, set to -1 for no timeout and get '
1324 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001325 parser.group_logging.add_option(
1326 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001327 parser.group_logging.add_option(
1328 '--print-status-updates', action='store_true',
1329 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001330 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001331 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001332 '--task-summary-json',
1333 metavar='FILE',
1334 help='Dump a summary of task results to this file as json. It contains '
1335 'only shards statuses as know to server directly. Any output files '
1336 'emitted by the task can be collected by using --task-output-dir')
1337 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001338 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001339 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001340 help='Directory to put task results into. When the task finishes, this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001341 'directory contains per-shard directory with output files produced '
1342 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001343 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001344 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001345 parser.task_output_group.add_option(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001346 '--filepath-filter',
1347 help='This is regexp filter used to specify downloaded filepath when '
1348 'collecting isolated output.')
1349 parser.task_output_group.add_option(
maruel9531ce02016-04-13 06:11:23 -07001350 '--perf', action='store_true', default=False,
1351 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001352 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001353
1354
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001355def process_collect_options(parser, options):
1356 # Only negative -1 is allowed, disallow other negative values.
1357 if options.timeout != -1 and options.timeout < 0:
1358 parser.error('Invalid --timeout value')
1359
1360
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001361@subcommand.usage('bots...')
1362def CMDbot_delete(parser, args):
1363 """Forcibly deletes bots from the Swarming server."""
1364 parser.add_option(
1365 '-f', '--force', action='store_true',
1366 help='Do not prompt for confirmation')
1367 options, args = parser.parse_args(args)
1368 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001369 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001370
1371 bots = sorted(args)
1372 if not options.force:
1373 print('Delete the following bots?')
1374 for bot in bots:
1375 print(' %s' % bot)
1376 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1377 print('Goodbye.')
1378 return 1
1379
1380 result = 0
1381 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001382 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001383 if net.url_read_json(url, data={}, method='POST') is None:
1384 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001385 result = 1
1386 return result
1387
1388
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001389def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001390 """Returns information about the bots connected to the Swarming server."""
1391 add_filter_options(parser)
1392 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001393 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001394 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001395 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001396 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001397 help='Keep both dead and alive bots')
1398 parser.filter_group.add_option(
1399 '--busy', action='store_true', help='Keep only busy bots')
1400 parser.filter_group.add_option(
1401 '--idle', action='store_true', help='Keep only idle bots')
1402 parser.filter_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001403 '--mp',
1404 action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001405 help='Keep only Machine Provider managed bots')
1406 parser.filter_group.add_option(
1407 '--non-mp', action='store_true',
1408 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001409 parser.filter_group.add_option(
1410 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001411 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001412 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001413 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001414
1415 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001416 parser.error('Use only one of --keep-dead or --dead-only')
1417 if options.busy and options.idle:
1418 parser.error('Use only one of --busy or --idle')
1419 if options.mp and options.non_mp:
1420 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001421
smut281c3902018-05-30 17:50:05 -07001422 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001423 values = []
1424 if options.dead_only:
1425 values.append(('is_dead', 'TRUE'))
1426 elif options.keep_dead:
1427 values.append(('is_dead', 'NONE'))
1428 else:
1429 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001430
maruelaf6b06c2017-06-08 06:26:53 -07001431 if options.busy:
1432 values.append(('is_busy', 'TRUE'))
1433 elif options.idle:
1434 values.append(('is_busy', 'FALSE'))
1435 else:
1436 values.append(('is_busy', 'NONE'))
1437
1438 if options.mp:
1439 values.append(('is_mp', 'TRUE'))
1440 elif options.non_mp:
1441 values.append(('is_mp', 'FALSE'))
1442 else:
1443 values.append(('is_mp', 'NONE'))
1444
1445 for key, value in options.dimensions:
1446 values.append(('dimensions', '%s:%s' % (key, value)))
Marc-Antoine Ruelad8cabe2019-10-10 23:24:26 +00001447 url += urllib.parse.urlencode(values)
maruelaf6b06c2017-06-08 06:26:53 -07001448 try:
1449 data, yielder = get_yielder(url, 0)
1450 bots = data.get('items') or []
1451 for items in yielder():
1452 if items:
1453 bots.extend(items)
1454 except Failure as e:
1455 sys.stderr.write('\n%s\n' % e)
1456 return 1
maruel77f720b2015-09-15 12:35:22 -07001457 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Lei Leife202df2019-06-11 17:33:34 +00001458 print(bot['bot_id'])
maruelaf6b06c2017-06-08 06:26:53 -07001459 if not options.bare:
1460 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Lei Leife202df2019-06-11 17:33:34 +00001461 print(' %s' % json.dumps(dimensions, sort_keys=True))
maruelaf6b06c2017-06-08 06:26:53 -07001462 if bot.get('task_id'):
Lei Leife202df2019-06-11 17:33:34 +00001463 print(' task: %s' % bot['task_id'])
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001464 return 0
1465
1466
maruelfd0a90c2016-06-10 11:51:10 -07001467@subcommand.usage('task_id')
1468def CMDcancel(parser, args):
1469 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001470 parser.add_option(
1471 '-k', '--kill-running', action='store_true', default=False,
1472 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001473 options, args = parser.parse_args(args)
1474 if not args:
1475 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001476 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001477 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001478 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001479 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001480 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001481 print('Deleting %s failed. Probably already gone' % task_id)
1482 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001483 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001484 return 0
1485
1486
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001487@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001488def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001489 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001490
1491 The result can be in multiple part if the execution was sharded. It can
1492 potentially have retries.
1493 """
1494 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001495 parser.add_option(
1496 '-j', '--json',
1497 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001498 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001499 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001500 if not args and not options.json:
1501 parser.error('Must specify at least one task id or --json.')
1502 if args and options.json:
1503 parser.error('Only use one of task id or --json.')
1504
1505 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001506 options.json = six.text_type(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001507 try:
maruel1ceb3872015-10-14 06:10:44 -07001508 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001509 data = json.load(f)
1510 except (IOError, ValueError):
1511 parser.error('Failed to open %s' % options.json)
1512 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001513 tasks = sorted(data['tasks'].values(), key=lambda x: x['shard_index'])
maruel71c61c82016-02-22 06:52:05 -08001514 args = [t['task_id'] for t in tasks]
1515 except (KeyError, TypeError):
1516 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001517 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001518 # Take in account all the task slices.
1519 offset = 0
1520 for s in data['request']['task_slices']:
1521 m = (offset + s['properties']['execution_timeout_secs'] +
1522 s['expiration_secs'])
1523 if m > options.timeout:
1524 options.timeout = m
1525 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001526 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001527 else:
1528 valid = frozenset('0123456789abcdef')
1529 if any(not valid.issuperset(task_id) for task_id in args):
1530 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001531
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001532 try:
Junji Watanabe38b28b02020-04-23 10:23:30 +00001533 return collect(options.swarming, args, options.timeout, options.decorate,
1534 options.print_status_updates, options.task_summary_json,
1535 options.task_output_dir, options.task_output_stdout,
1536 options.perf, options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001537 except Failure:
1538 on_error.report(None)
1539 return 1
1540
1541
maruel77f720b2015-09-15 12:35:22 -07001542@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001543def CMDpost(parser, args):
1544 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1545
1546 Input data must be sent to stdin, result is printed to stdout.
1547
1548 If HTTP response code >= 400, returns non-zero.
1549 """
1550 options, args = parser.parse_args(args)
1551 if len(args) != 1:
1552 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001553 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001554 data = sys.stdin.read()
1555 try:
1556 resp = net.url_read(url, data=data, method='POST')
1557 except net.TimeoutError:
1558 sys.stderr.write('Timeout!\n')
1559 return 1
1560 if not resp:
1561 sys.stderr.write('No response!\n')
1562 return 1
1563 sys.stdout.write(resp)
1564 return 0
1565
1566
1567@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001568def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001569 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1570 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001571
1572 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001573 Raw task request and results:
1574 swarming.py query -S server-url.com task/123456/request
1575 swarming.py query -S server-url.com task/123456/result
1576
maruel77f720b2015-09-15 12:35:22 -07001577 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001578 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001579
maruelaf6b06c2017-06-08 06:26:53 -07001580 Listing last 10 tasks on a specific bot named 'bot1':
1581 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001582
maruelaf6b06c2017-06-08 06:26:53 -07001583 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001584 quoting is important!:
1585 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001586 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001587 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001588 parser.add_option(
1589 '-L', '--limit', type='int', default=200,
1590 help='Limit to enforce on limitless items (like number of tasks); '
1591 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001592 parser.add_option(
1593 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001594 parser.add_option(
1595 '--progress', action='store_true',
1596 help='Prints a dot at each request to show progress')
1597 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001598 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001599 parser.error(
1600 'Must specify only method name and optionally query args properly '
1601 'escaped.')
smut281c3902018-05-30 17:50:05 -07001602 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001603 try:
1604 data, yielder = get_yielder(base_url, options.limit)
1605 for items in yielder():
1606 if items:
1607 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001608 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001609 sys.stderr.write('.')
1610 sys.stderr.flush()
1611 except Failure as e:
1612 sys.stderr.write('\n%s\n' % e)
1613 return 1
maruel77f720b2015-09-15 12:35:22 -07001614 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001615 sys.stderr.write('\n')
1616 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001617 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001618 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001619 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001620 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001621 try:
maruel77f720b2015-09-15 12:35:22 -07001622 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001623 sys.stdout.write('\n')
1624 except IOError:
1625 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001626 return 0
1627
1628
maruel77f720b2015-09-15 12:35:22 -07001629def CMDquery_list(parser, args):
1630 """Returns list of all the Swarming APIs that can be used with command
1631 'query'.
1632 """
1633 parser.add_option(
1634 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1635 options, args = parser.parse_args(args)
1636 if args:
1637 parser.error('No argument allowed.')
1638
1639 try:
1640 apis = endpoints_api_discovery_apis(options.swarming)
1641 except APIError as e:
1642 parser.error(str(e))
1643 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001644 options.json = six.text_type(os.path.abspath(options.json))
maruel1ceb3872015-10-14 06:10:44 -07001645 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001646 json.dump(apis, f)
1647 else:
1648 help_url = (
1649 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1650 options.swarming)
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001651 for i, (api_id, api) in enumerate(sorted(apis.items())):
maruel11e31af2017-02-15 07:30:50 -08001652 if i:
1653 print('')
Lei Leife202df2019-06-11 17:33:34 +00001654 print(api_id)
1655 print(' ' + api['description'].strip())
maruel11e31af2017-02-15 07:30:50 -08001656 if 'resources' in api:
1657 # Old.
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001658 # TODO(maruel): Remove.
1659 # pylint: disable=too-many-nested-blocks
maruel11e31af2017-02-15 07:30:50 -08001660 for j, (resource_name, resource) in enumerate(
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001661 sorted(api['resources'].items())):
maruel11e31af2017-02-15 07:30:50 -08001662 if j:
1663 print('')
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001664 for method_name, method in sorted(resource['methods'].items()):
maruel11e31af2017-02-15 07:30:50 -08001665 # Only list the GET ones.
1666 if method['httpMethod'] != 'GET':
1667 continue
Lei Leife202df2019-06-11 17:33:34 +00001668 print('- %s.%s: %s' % (
1669 resource_name, method_name, method['path']))
maruel11e31af2017-02-15 07:30:50 -08001670 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001671 ' ' + l for l in textwrap.wrap(
1672 method.get('description', 'No description'), 78)))
Lei Leife202df2019-06-11 17:33:34 +00001673 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel11e31af2017-02-15 07:30:50 -08001674 else:
1675 # New.
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001676 for method_name, method in sorted(api['methods'].items()):
maruel77f720b2015-09-15 12:35:22 -07001677 # Only list the GET ones.
1678 if method['httpMethod'] != 'GET':
1679 continue
Lei Leife202df2019-06-11 17:33:34 +00001680 print('- %s: %s' % (method['id'], method['path']))
maruel11e31af2017-02-15 07:30:50 -08001681 print('\n'.join(
1682 ' ' + l for l in textwrap.wrap(method['description'], 78)))
Lei Leife202df2019-06-11 17:33:34 +00001683 print(' %s%s%s' % (help_url, api['servicePath'], method['id']))
maruel77f720b2015-09-15 12:35:22 -07001684 return 0
1685
1686
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001687@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001688def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001689 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001690
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001691 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001692 """
1693 add_trigger_options(parser)
1694 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001695 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001696 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001697 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001698 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001699 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001700 tasks = trigger_task_shards(
1701 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001702 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001703 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001704 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001705 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001706 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001707 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001708 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001709 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001710 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001711 task_ids = [
Junji Watanabe38b28b02020-04-23 10:23:30 +00001712 t['task_id']
1713 for t in sorted(tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001714 ]
Caleb Rouleau779c4f02019-05-22 21:18:49 +00001715 for task_id in task_ids:
1716 print('Task: {server}/task?id={task}'.format(
1717 server=options.swarming, task=task_id))
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001718 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001719 offset = 0
1720 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001721 m = (offset + s.properties.execution_timeout_secs +
1722 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001723 if m > options.timeout:
1724 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001725 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001726 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001727 try:
1728 return collect(
1729 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001730 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001731 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001732 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001733 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001734 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001735 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001736 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001737 options.perf,
1738 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001739 except Failure:
1740 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001741 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001742
1743
maruel18122c62015-10-23 06:31:23 -07001744@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001745def CMDreproduce(parser, args):
1746 """Runs a task locally that was triggered on the server.
1747
1748 This running locally the same commands that have been run on the bot. The data
1749 downloaded will be in a subdirectory named 'work' of the current working
1750 directory.
maruel18122c62015-10-23 06:31:23 -07001751
1752 You can pass further additional arguments to the target command by passing
1753 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001754 """
maruelc070e672016-02-22 17:32:57 -08001755 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001756 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001757 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001758 parser.add_option(
1759 '--work', metavar='DIR', default='work',
1760 help='Directory to map the task input files into')
1761 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001762 '--cache',
1763 metavar='DIR',
1764 default='cache',
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001765 help='Directory that contains the input cache')
1766 parser.add_option(
1767 '--leak', action='store_true',
1768 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001769 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001770 extra_args = []
1771 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001772 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001773 if len(args) > 1:
1774 if args[1] == '--':
1775 if len(args) > 2:
1776 extra_args = args[2:]
1777 else:
1778 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001779
smut281c3902018-05-30 17:50:05 -07001780 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001781 request = net.url_read_json(url)
1782 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001783 print('Failed to retrieve request data for the task', file=sys.stderr)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001784 return 1
1785
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001786 workdir = six.text_type(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001787 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001788 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001789 fs.mkdir(workdir)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001790 cachedir = six.text_type(os.path.abspath('cipd_cache'))
iannucci31ab9192017-05-02 19:11:56 -07001791 if not fs.exists(cachedir):
1792 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001793
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001794 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001795 env = os.environ.copy()
1796 env['SWARMING_BOT_ID'] = 'reproduce'
1797 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001798 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001799 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001800 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001801 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001802 if not i['value']:
1803 env.pop(key, None)
1804 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001805 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001806
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001807 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001808 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001809 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001810 for i in env_prefixes:
1811 key = i['key']
1812 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001813 cur = env.get(key)
1814 if cur:
1815 paths.append(cur)
1816 env[key] = os.path.pathsep.join(paths)
1817
iannucci31ab9192017-05-02 19:11:56 -07001818 command = []
nodir152cba62016-05-12 16:08:56 -07001819 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001820 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001821 server_ref = isolate_storage.ServerRef(
maruel29ab2fd2015-10-16 11:44:01 -07001822 properties['inputs_ref']['isolatedserver'],
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001823 properties['inputs_ref']['namespace'])
1824 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001825 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1826 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1827 # leak.
1828 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001829 cache = local_caching.DiskContentAddressedCache(
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001830 six.text_type(os.path.abspath(options.cache)), policies, False)
maruel29ab2fd2015-10-16 11:44:01 -07001831 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001832 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001833 command = bundle.command
1834 if bundle.relative_cwd:
1835 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001836 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001837
1838 if properties.get('command'):
1839 command.extend(properties['command'])
1840
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001841 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Brian Sheedy7a761172019-08-30 22:55:14 +00001842 command = tools.find_executable(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001843 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001844 new_command = run_isolated.process_command(command, 'invalid', None)
1845 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001846 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001847 else:
1848 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001849 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001850 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001851 command, options.output, None)
1852 if not os.path.isdir(options.output):
1853 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001854 command = new_command
1855 file_path.ensure_command_has_abs_path(command, workdir)
1856
1857 if properties.get('cipd_input'):
1858 ci = properties['cipd_input']
1859 cp = ci['client_package']
1860 client_manager = cipd.get_client(
1861 ci['server'], cp['package_name'], cp['version'], cachedir)
1862
1863 with client_manager as client:
1864 by_path = collections.defaultdict(list)
1865 for pkg in ci['packages']:
1866 path = pkg['path']
1867 # cipd deals with 'root' as ''
1868 if path == '.':
1869 path = ''
1870 by_path[path].append((pkg['package_name'], pkg['version']))
1871 client.ensure(workdir, by_path, cache_dir=cachedir)
1872
maruel77f720b2015-09-15 12:35:22 -07001873 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001874 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001875 except OSError as e:
Lei Leife202df2019-06-11 17:33:34 +00001876 print('Failed to run: %s' % ' '.join(command), file=sys.stderr)
1877 print(str(e), file=sys.stderr)
maruel77f720b2015-09-15 12:35:22 -07001878 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001879 finally:
1880 # Do not delete options.cache.
1881 if not options.leak:
1882 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001883
1884
maruel0eb1d1b2015-10-02 14:48:21 -07001885@subcommand.usage('bot_id')
1886def CMDterminate(parser, args):
1887 """Tells a bot to gracefully shut itself down as soon as it can.
1888
1889 This is done by completing whatever current task there is then exiting the bot
1890 process.
1891 """
1892 parser.add_option(
1893 '--wait', action='store_true', help='Wait for the bot to terminate')
1894 options, args = parser.parse_args(args)
1895 if len(args) != 1:
1896 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001897 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001898 request = net.url_read_json(url, data={})
1899 if not request:
Lei Leife202df2019-06-11 17:33:34 +00001900 print('Failed to ask for termination', file=sys.stderr)
maruel0eb1d1b2015-10-02 14:48:21 -07001901 return 1
1902 if options.wait:
1903 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001904 options.swarming,
1905 [request['task_id']],
1906 0.,
1907 False,
1908 False,
1909 None,
1910 None,
1911 [],
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001912 False,
1913 None)
maruelbfc5f872017-06-10 16:43:17 -07001914 else:
Lei Leife202df2019-06-11 17:33:34 +00001915 print(request['task_id'])
maruel0eb1d1b2015-10-02 14:48:21 -07001916 return 0
1917
1918
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001919@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001920def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001921 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001922
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001923 Passes all extra arguments provided after '--' as additional command line
1924 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001925 """
1926 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001927 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001928 parser.add_option(
1929 '--dump-json',
1930 metavar='FILE',
1931 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001932 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001933 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001934 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001935 tasks = trigger_task_shards(
1936 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001937 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001938 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001939 tasks_sorted = sorted(
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001940 tasks.values(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001941 if options.dump_json:
1942 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001943 'base_task_name': task_request.name,
1944 'tasks': tasks,
1945 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001946 }
maruel46b015f2015-10-13 18:40:35 -07001947 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001948 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001949 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001950 (options.swarming, options.dump_json))
1951 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001952 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001953 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001954 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1955 print('Or visit:')
1956 for t in tasks_sorted:
1957 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001958 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001959 except Failure:
1960 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001961 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001962
1963
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001964class OptionParserSwarming(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +00001965
maruel@chromium.org0437a732013-08-27 16:05:52 +00001966 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001967 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001968 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001969 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001970 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001971 '-S',
1972 '--swarming',
1973 metavar='URL',
1974 default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001975 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001976 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001977 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001978
1979 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001980 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001981 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001982 auth.process_auth_options(self, options)
1983 user = self._process_swarming(options)
1984 if hasattr(options, 'user') and not options.user:
1985 options.user = user
1986 return options, args
1987
1988 def _process_swarming(self, options):
1989 """Processes the --swarming option and aborts if not specified.
1990
1991 Returns the identity as determined by the server.
1992 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001993 if not options.swarming:
1994 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001995 try:
1996 options.swarming = net.fix_url(options.swarming)
1997 except ValueError as e:
1998 self.error('--swarming %s' % e)
Takuto Ikutaae767b32020-05-11 01:22:19 +00001999
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05002000 try:
2001 user = auth.ensure_logged_in(options.swarming)
2002 except ValueError as e:
2003 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05002004 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00002005
2006
2007def main(args):
2008 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04002009 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002010
2011
2012if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07002013 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00002014 fix_encoding.fix_encoding()
2015 tools.disable_buffering()
2016 colorama.init()
Takuto Ikuta7c843c82020-04-15 05:42:54 +00002017 net.set_user_agent('swarming.py/' + __version__)
maruel@chromium.org0437a732013-08-27 16:05:52 +00002018 sys.exit(main(sys.argv[1:]))