blob: b8f86ba156944775a7b96c28b1dee75b092d5fd9 [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
Robert Iannuccifafa7352018-06-13 17:08:17 +00008__version__ = '0.13'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import sys
maruel11e31af2017-02-15 07:30:50 -080018import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
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 Ruel5aeb3bb2018-06-16 13:11:02 +000040import isolated_format
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040041import local_caching
maruelc070e672016-02-22 17:32:57 -080042import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000043
44
tansella4949442016-06-23 22:34:32 -070045ROOT_DIR = os.path.dirname(os.path.abspath(
46 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050047
48
49class Failure(Exception):
50 """Generic failure."""
51 pass
52
53
maruel0a25f6c2017-05-10 10:43:23 -070054def default_task_name(options):
55 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050056 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070057 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070058 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070059 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070060 if options.isolated:
61 task_name += u'/' + options.isolated
62 return task_name
63 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050064
65
66### Triggering.
67
68
maruel77f720b2015-09-15 12:35:22 -070069# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070070CipdPackage = collections.namedtuple(
71 'CipdPackage',
72 [
73 'package_name',
74 'path',
75 'version',
76 ])
77
78
79# See ../appengine/swarming/swarming_rpcs.py.
80CipdInput = collections.namedtuple(
81 'CipdInput',
82 [
83 'client_package',
84 'packages',
85 'server',
86 ])
87
88
89# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070090FilesRef = collections.namedtuple(
91 'FilesRef',
92 [
93 'isolated',
94 'isolatedserver',
95 'namespace',
96 ])
97
98
99# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800100StringListPair = collections.namedtuple(
101 'StringListPair', [
102 'key',
103 'value', # repeated string
104 ]
105)
106
107
108# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700109TaskProperties = collections.namedtuple(
110 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500111 [
maruel681d6802017-01-17 16:56:03 -0800112 'caches',
borenet02f772b2016-06-22 12:42:19 -0700113 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500114 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500115 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500116 'dimensions',
117 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800118 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700119 'execution_timeout_secs',
120 'extra_args',
121 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500122 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700123 'inputs_ref',
124 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700125 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700126 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700127 ])
128
129
130# See ../appengine/swarming/swarming_rpcs.py.
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400131TaskSlice = collections.namedtuple(
132 'TaskSlice',
133 [
134 'expiration_secs',
135 'properties',
136 'wait_for_capacity',
137 ])
138
139
140# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700141NewTaskRequest = collections.namedtuple(
142 'NewTaskRequest',
143 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144 'name',
maruel77f720b2015-09-15 12:35:22 -0700145 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500146 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400147 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700148 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500149 'tags',
150 'user',
Robert Iannuccifafa7352018-06-13 17:08:17 +0000151 'pool_task_template',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500152 ])
153
154
maruel77f720b2015-09-15 12:35:22 -0700155def namedtuple_to_dict(value):
156 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400157 if hasattr(value, '_asdict'):
158 return namedtuple_to_dict(value._asdict())
159 if isinstance(value, (list, tuple)):
160 return [namedtuple_to_dict(v) for v in value]
161 if isinstance(value, dict):
162 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
163 return value
maruel77f720b2015-09-15 12:35:22 -0700164
165
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700166def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800167 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700168
169 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170 """
maruel77f720b2015-09-15 12:35:22 -0700171 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700172 # Don't send 'service_account' if it is None to avoid confusing older
173 # version of the server that doesn't know about 'service_account' and don't
174 # use it at all.
175 if not out['service_account']:
176 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000177 for task_slice in out['task_slices']:
178 task_slice['properties']['env'] = [
179 {'key': k, 'value': v}
180 for k, v in task_slice['properties']['env'].iteritems()
181 ]
182 task_slice['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700183 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500184
185
maruel77f720b2015-09-15 12:35:22 -0700186def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500187 """Triggers a request on the Swarming server and returns the json data.
188
189 It's the low-level function.
190
191 Returns:
192 {
193 'request': {
194 'created_ts': u'2010-01-02 03:04:05',
195 'name': ..
196 },
197 'task_id': '12300',
198 }
199 """
200 logging.info('Triggering: %s', raw_request['name'])
201
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500202 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700203 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500204 if not result:
205 on_error.report('Failed to trigger task %s' % raw_request['name'])
206 return None
maruele557bce2015-11-17 09:01:27 -0800207 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800208 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800209 msg = 'Failed to trigger task %s' % raw_request['name']
210 if result['error'].get('errors'):
211 for err in result['error']['errors']:
212 if err.get('message'):
213 msg += '\nMessage: %s' % err['message']
214 if err.get('debugInfo'):
215 msg += '\nDebug info:\n%s' % err['debugInfo']
216 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800217 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800218
219 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800220 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500221 return result
222
223
224def setup_googletest(env, shards, index):
225 """Sets googletest specific environment variables."""
226 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700227 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
228 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
229 env = env[:]
230 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
231 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500232 return env
233
234
235def trigger_task_shards(swarming, task_request, shards):
236 """Triggers one or many subtasks of a sharded task.
237
238 Returns:
239 Dict with task details, returned to caller as part of --dump-json output.
240 None in case of failure.
241 """
242 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700243 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500244 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400245 req['task_slices'][0]['properties']['env'] = setup_googletest(
246 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700247 req['name'] += ':%s:%s' % (index, shards)
248 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500249
250 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251 tasks = {}
252 priority_warning = False
253 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700254 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500255 if not task:
256 break
257 logging.info('Request result: %s', task)
258 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400259 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500260 priority_warning = True
261 print >> sys.stderr, (
262 'Priority was reset to %s' % task['request']['priority'])
263 tasks[request['name']] = {
264 'shard_index': index,
265 'task_id': task['task_id'],
266 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
267 }
268
269 # Some shards weren't triggered. Abort everything.
270 if len(tasks) != len(requests):
271 if tasks:
272 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
273 len(tasks), len(requests))
274 for task_dict in tasks.itervalues():
275 abort_task(swarming, task_dict['task_id'])
276 return None
277
278 return tasks
279
280
281### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000282
283
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700284# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000285STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700286
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400287
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000288class TaskState(object):
289 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000290
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000291 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
292 is the source of truth for these values:
293 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400294
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000295 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400296 """
297 RUNNING = 0x10
298 PENDING = 0x20
299 EXPIRED = 0x30
300 TIMED_OUT = 0x40
301 BOT_DIED = 0x50
302 CANCELED = 0x60
303 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400304 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400305 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400306
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000307 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400308
maruel77f720b2015-09-15 12:35:22 -0700309 _ENUMS = {
310 'RUNNING': RUNNING,
311 'PENDING': PENDING,
312 'EXPIRED': EXPIRED,
313 'TIMED_OUT': TIMED_OUT,
314 'BOT_DIED': BOT_DIED,
315 'CANCELED': CANCELED,
316 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400317 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400318 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700319 }
320
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400321 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700322 def from_enum(cls, state):
323 """Returns int value based on the string."""
324 if state not in cls._ENUMS:
325 raise ValueError('Invalid state %s' % state)
326 return cls._ENUMS[state]
327
maruel@chromium.org0437a732013-08-27 16:05:52 +0000328
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700329class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700330 """Assembles task execution summary (for --task-summary-json output).
331
332 Optionally fetches task outputs from isolate server to local disk (used when
333 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700334
335 This object is shared among multiple threads running 'retrieve_results'
336 function, in particular they call 'process_shard_result' method in parallel.
337 """
338
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000339 def __init__(self, task_output_dir, task_output_stdout, shard_count,
340 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700341 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
342
343 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700344 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700345 shard_count: expected number of task shards.
346 """
maruel12e30012015-10-09 11:55:35 -0700347 self.task_output_dir = (
348 unicode(os.path.abspath(task_output_dir))
349 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000350 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700351 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000352 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353
354 self._lock = threading.Lock()
355 self._per_shard_results = {}
356 self._storage = None
357
nodire5028a92016-04-29 14:38:21 -0700358 if self.task_output_dir:
359 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360
Vadim Shtayurab450c602014-05-12 19:23:25 -0700361 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362 """Stores results of a single task shard, fetches output files if necessary.
363
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400364 Modifies |result| in place.
365
maruel77f720b2015-09-15 12:35:22 -0700366 shard_index is 0-based.
367
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368 Called concurrently from multiple threads.
369 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700371 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 if shard_index < 0 or shard_index >= self.shard_count:
373 logging.warning(
374 'Shard index %d is outside of expected range: [0; %d]',
375 shard_index, self.shard_count - 1)
376 return
377
maruel77f720b2015-09-15 12:35:22 -0700378 if result.get('outputs_ref'):
379 ref = result['outputs_ref']
380 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
381 ref['isolatedserver'],
382 urllib.urlencode(
383 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400384
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 # Store result dict of that shard, ignore results we've already seen.
386 with self._lock:
387 if shard_index in self._per_shard_results:
388 logging.warning('Ignoring duplicate shard index %d', shard_index)
389 return
390 self._per_shard_results[shard_index] = result
391
392 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700393 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400394 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700395 result['outputs_ref']['isolatedserver'],
396 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400397 if storage:
398 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400399 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
400 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400401 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700402 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400403 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400404 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700405 os.path.join(self.task_output_dir, str(shard_index)),
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000406 False, self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700407
408 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700409 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700410 with self._lock:
411 # Write an array of shard results with None for missing shards.
412 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700413 'shards': [
414 self._per_shard_results.get(i) for i in xrange(self.shard_count)
415 ],
416 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000417
418 # Don't store stdout in the summary if not requested too.
419 if "json" not in self.task_output_stdout:
420 for shard_json in summary['shards']:
421 if not shard_json:
422 continue
423 if "output" in shard_json:
424 del shard_json["output"]
425 if "outputs" in shard_json:
426 del shard_json["outputs"]
427
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700428 # Write summary.json to task_output_dir as well.
429 if self.task_output_dir:
430 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700431 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700432 summary,
433 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700434 if self._storage:
435 self._storage.close()
436 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700437 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438
439 def _get_storage(self, isolate_server, namespace):
440 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700441 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442 with self._lock:
443 if not self._storage:
444 self._storage = isolateserver.get_storage(isolate_server, namespace)
445 else:
446 # Shards must all use exact same isolate server and namespace.
447 if self._storage.location != isolate_server:
448 logging.error(
449 'Task shards are using multiple isolate servers: %s and %s',
450 self._storage.location, isolate_server)
451 return None
452 if self._storage.namespace != namespace:
453 logging.error(
454 'Task shards are using multiple namespaces: %s and %s',
455 self._storage.namespace, namespace)
456 return None
457 return self._storage
458
459
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500460def now():
461 """Exists so it can be mocked easily."""
462 return time.time()
463
464
maruel77f720b2015-09-15 12:35:22 -0700465def parse_time(value):
466 """Converts serialized time from the API to datetime.datetime."""
467 # When microseconds are 0, the '.123456' suffix is elided. This means the
468 # serialized format is not consistent, which confuses the hell out of python.
469 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
470 try:
471 return datetime.datetime.strptime(value, fmt)
472 except ValueError:
473 pass
474 raise ValueError('Failed to parse %s' % value)
475
476
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700477def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700478 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000479 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400480 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700481
Vadim Shtayurab450c602014-05-12 19:23:25 -0700482 Returns:
483 <result dict> on success.
484 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700485 """
maruel71c61c82016-02-22 06:52:05 -0800486 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700487 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700488 if include_perf:
489 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700490 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700491 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400492 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700493 attempt = 0
494
495 while not should_stop.is_set():
496 attempt += 1
497
498 # Waiting for too long -> give up.
499 current_time = now()
500 if deadline and current_time >= deadline:
501 logging.error('retrieve_results(%s) timed out on attempt %d',
502 base_url, attempt)
503 return None
504
505 # Do not spin too fast. Spin faster at the beginning though.
506 # Start with 1 sec delay and for each 30 sec of waiting add another second
507 # of delay, until hitting 15 sec ceiling.
508 if attempt > 1:
509 max_delay = min(15, 1 + (current_time - started) / 30.0)
510 delay = min(max_delay, deadline - current_time) if deadline else max_delay
511 if delay > 0:
512 logging.debug('Waiting %.1f sec before retrying', delay)
513 should_stop.wait(delay)
514 if should_stop.is_set():
515 return None
516
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400517 # Disable internal retries in net.url_read_json, since we are doing retries
518 # ourselves.
519 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700520 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
521 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400522 # Retry on 500s only if no timeout is specified.
523 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400524 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400525 if timeout == -1:
526 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400527 continue
maruel77f720b2015-09-15 12:35:22 -0700528
maruelbf53e042015-12-01 15:00:51 -0800529 if result.get('error'):
530 # An error occurred.
531 if result['error'].get('errors'):
532 for err in result['error']['errors']:
533 logging.warning(
534 'Error while reading task: %s; %s',
535 err.get('message'), err.get('debugInfo'))
536 elif result['error'].get('message'):
537 logging.warning(
538 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400539 if timeout == -1:
540 return result
maruelbf53e042015-12-01 15:00:51 -0800541 continue
542
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400543 # When timeout == -1, always return on first attempt. 500s are already
544 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000545 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000546 if fetch_stdout:
547 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700548 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700549 # Record the result, try to fetch attached output files (if any).
550 if output_collector:
551 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700552 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700553 if result.get('internal_failure'):
554 logging.error('Internal error!')
555 elif result['state'] == 'BOT_DIED':
556 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700557 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000558
559
maruel77f720b2015-09-15 12:35:22 -0700560def convert_to_old_format(result):
561 """Converts the task result data from Endpoints API format to old API format
562 for compatibility.
563
564 This goes into the file generated as --task-summary-json.
565 """
566 # Sets default.
567 result.setdefault('abandoned_ts', None)
568 result.setdefault('bot_id', None)
569 result.setdefault('bot_version', None)
570 result.setdefault('children_task_ids', [])
571 result.setdefault('completed_ts', None)
572 result.setdefault('cost_saved_usd', None)
573 result.setdefault('costs_usd', None)
574 result.setdefault('deduped_from', None)
575 result.setdefault('name', None)
576 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700577 result.setdefault('server_versions', None)
578 result.setdefault('started_ts', None)
579 result.setdefault('tags', None)
580 result.setdefault('user', None)
581
582 # Convertion back to old API.
583 duration = result.pop('duration', None)
584 result['durations'] = [duration] if duration else []
585 exit_code = result.pop('exit_code', None)
586 result['exit_codes'] = [int(exit_code)] if exit_code else []
587 result['id'] = result.pop('task_id')
588 result['isolated_out'] = result.get('outputs_ref', None)
589 output = result.pop('output', None)
590 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700591 # server_version
592 # Endpoints result 'state' as string. For compatibility with old code, convert
593 # to int.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000594 result['state'] = TaskState.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700595 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700596 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700597 if 'bot_dimensions' in result:
598 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700599 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700600 }
601 else:
602 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700603
604
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700605def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400606 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000607 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500608 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000609
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700610 Duplicate shards are ignored. Shards are yielded in order of completion.
611 Timed out shards are NOT yielded at all. Caller can compare number of yielded
612 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000613
614 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500615 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 +0000616 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500617
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700618 output_collector is an optional instance of TaskOutputCollector that will be
619 used to fetch files produced by a task from isolate server to the local disk.
620
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500621 Yields:
622 (index, result). In particular, 'result' is defined as the
623 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400626 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700627 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700628 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700629
maruel@chromium.org0437a732013-08-27 16:05:52 +0000630 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
631 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700632 # Adds a task to the thread pool to call 'retrieve_results' and return
633 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400634 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000635 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700636 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000637 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400638 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000639 task_id, timeout, should_stop, output_collector, include_perf,
640 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700641
642 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400643 for shard_index, task_id in enumerate(task_ids):
644 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700645
646 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400647 shards_remaining = range(len(task_ids))
648 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700651 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652 shard_index, result = results_channel.pull(
653 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700654 except threading_utils.TaskChannel.Timeout:
655 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000656 time_now = str(datetime.datetime.now())
657 _, time_now = time_now.split(' ')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700658 print(
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000659 '%s '
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 'Waiting for results from the following shards: %s' %
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000661 (time_now, ', '.join(map(str, shards_remaining)))
662 )
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 sys.stdout.flush()
664 continue
665 except Exception:
666 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700667
668 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700669 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000670 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500671 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000672 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700673
Vadim Shtayurab450c602014-05-12 19:23:25 -0700674 # Yield back results to the caller.
675 assert shard_index in shards_remaining
676 shards_remaining.remove(shard_index)
677 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700678
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700680 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 should_stop.set()
682
683
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000684def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000685 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700686 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400687 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700688 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
689 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400690 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
691 metadata.get('abandoned_ts')):
692 pending = '%.1fs' % (
693 parse_time(metadata['abandoned_ts']) -
694 parse_time(metadata['created_ts'])
695 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400696 else:
697 pending = 'N/A'
698
maruel77f720b2015-09-15 12:35:22 -0700699 if metadata.get('duration') is not None:
700 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400701 else:
702 duration = 'N/A'
703
maruel77f720b2015-09-15 12:35:22 -0700704 if metadata.get('exit_code') is not None:
705 # Integers are encoded as string to not loose precision.
706 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400707 else:
708 exit_code = 'N/A'
709
710 bot_id = metadata.get('bot_id') or 'N/A'
711
maruel77f720b2015-09-15 12:35:22 -0700712 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400713 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000714 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400715 if metadata.get('state') == 'CANCELED':
716 tag_footer2 = ' Pending: %s CANCELED' % pending
717 elif metadata.get('state') == 'EXPIRED':
718 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400719 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400720 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
721 pending, duration, bot_id, exit_code, metadata['state'])
722 else:
723 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
724 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400725
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000726 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
727 dash_pad = '+-%s-+' % ('-' * tag_len)
728 tag_header = '| %s |' % tag_header.ljust(tag_len)
729 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
730 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400731
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000732 if include_stdout:
733 return '\n'.join([
734 dash_pad,
735 tag_header,
736 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400737 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000738 dash_pad,
739 tag_footer1,
740 tag_footer2,
741 dash_pad,
742 ])
743 else:
744 return '\n'.join([
745 dash_pad,
746 tag_header,
747 tag_footer2,
748 dash_pad,
749 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000750
751
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700752def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700753 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000754 task_summary_json, task_output_dir, task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000755 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700756 """Retrieves results of a Swarming task.
757
758 Returns:
759 process exit code that should be returned to the user.
760 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000761
762 filter_cb = None
763 if filepath_filter:
764 filter_cb = re.compile(filepath_filter).match
765
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700766 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000767 output_collector = TaskOutputCollector(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000768 task_output_dir, task_output_stdout, len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700770 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700771 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400772 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400774 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400775 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000776 output_collector, include_perf,
777 (len(task_output_stdout) > 0),
778 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700779 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700780
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400781 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700782 shard_exit_code = metadata.get('exit_code')
783 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700784 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700785 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700786 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400787 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700788 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700789
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000791 s = decorate_shard_output(
792 swarming, index, metadata,
793 "console" in task_output_stdout).encode(
794 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700795 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400796 if len(seen_shards) < len(task_ids):
797 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700798 else:
maruel77f720b2015-09-15 12:35:22 -0700799 print('%s: %s %s' % (
800 metadata.get('bot_id', 'N/A'),
801 metadata['task_id'],
802 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000803 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700804 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400805 if output:
806 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700807 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700808 summary = output_collector.finalize()
809 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700810 # TODO(maruel): Make this optional.
811 for i in summary['shards']:
812 if i:
813 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700814 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700815
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400816 if decorate and total_duration:
817 print('Total duration: %.1fs' % total_duration)
818
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400819 if len(seen_shards) != len(task_ids):
820 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700821 print >> sys.stderr, ('Results from some shards are missing: %s' %
822 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700823 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700824
maruela5490782015-09-30 10:56:59 -0700825 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000826
827
maruel77f720b2015-09-15 12:35:22 -0700828### API management.
829
830
831class APIError(Exception):
832 pass
833
834
835def endpoints_api_discovery_apis(host):
836 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
837 the APIs exposed by a host.
838
839 https://developers.google.com/discovery/v1/reference/apis/list
840 """
maruel380e3262016-08-31 16:10:06 -0700841 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
842 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700843 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
844 if data is None:
845 raise APIError('Failed to discover APIs on %s' % host)
846 out = {}
847 for api in data['items']:
848 if api['id'] == 'discovery:v1':
849 continue
850 # URL is of the following form:
851 # url = host + (
852 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
853 api_data = net.url_read_json(api['discoveryRestUrl'])
854 if api_data is None:
855 raise APIError('Failed to discover %s on %s' % (api['id'], host))
856 out[api['id']] = api_data
857 return out
858
859
maruelaf6b06c2017-06-08 06:26:53 -0700860def get_yielder(base_url, limit):
861 """Returns the first query and a function that yields following items."""
862 CHUNK_SIZE = 250
863
864 url = base_url
865 if limit:
866 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
867 data = net.url_read_json(url)
868 if data is None:
869 # TODO(maruel): Do basic diagnostic.
870 raise Failure('Failed to access %s' % url)
871 org_cursor = data.pop('cursor', None)
872 org_total = len(data.get('items') or [])
873 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
874 if not org_cursor or not org_total:
875 # This is not an iterable resource.
876 return data, lambda: []
877
878 def yielder():
879 cursor = org_cursor
880 total = org_total
881 # Some items support cursors. Try to get automatically if cursors are needed
882 # by looking at the 'cursor' items.
883 while cursor and (not limit or total < limit):
884 merge_char = '&' if '?' in base_url else '?'
885 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
886 if limit:
887 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
888 new = net.url_read_json(url)
889 if new is None:
890 raise Failure('Failed to access %s' % url)
891 cursor = new.get('cursor')
892 new_items = new.get('items')
893 nb_items = len(new_items or [])
894 total += nb_items
895 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
896 yield new_items
897
898 return data, yielder
899
900
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500901### Commands.
902
903
904def abort_task(_swarming, _manifest):
905 """Given a task manifest that was triggered, aborts its execution."""
906 # TODO(vadimsh): No supported by the server yet.
907
908
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800910 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500911 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500912 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500913 dest='dimensions', metavar='FOO bar',
914 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000915 parser.filter_group.add_option(
916 '--optional-dimension', default=[], action='append', nargs=3,
917 dest='optional_dimensions', metavar='key value expiration',
918 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500919 parser.add_option_group(parser.filter_group)
920
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400921
Brad Hallf78187a2018-10-19 17:08:55 +0000922def _validate_filter_option(parser, key, value, expiration, argname):
923 if ':' in key:
924 parser.error('%s key cannot contain ":"' % argname)
925 if key.strip() != key:
926 parser.error('%s key has whitespace' % argname)
927 if not key:
928 parser.error('%s key is empty' % argname)
929
930 if value.strip() != value:
931 parser.error('%s value has whitespace' % argname)
932 if not value:
933 parser.error('%s value is empty' % argname)
934
935 if expiration is not None:
936 try:
937 expiration = int(expiration)
938 except ValueError:
939 parser.error('%s expiration is not an integer' % argname)
940 if expiration <= 0:
941 parser.error('%s expiration should be positive' % argname)
942 if expiration % 60 != 0:
943 parser.error('%s expiration is not divisible by 60' % argname)
944
945
maruelaf6b06c2017-06-08 06:26:53 -0700946def process_filter_options(parser, options):
947 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000948 _validate_filter_option(parser, key, value, None, 'dimension')
949 for key, value, exp in options.optional_dimensions:
950 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700951 options.dimensions.sort()
952
953
Vadim Shtayurab450c602014-05-12 19:23:25 -0700954def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400955 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700956 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700957 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700958 help='Number of shards to trigger and collect.')
959 parser.add_option_group(parser.sharding_group)
960
961
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400962def add_trigger_options(parser):
963 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500964 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400965 add_filter_options(parser)
966
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400967 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800968 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700969 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500970 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500972 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700973 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800974 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800975 '--env-prefix', default=[], action='append', nargs=2,
976 metavar='VAR local/path',
977 help='Prepend task-relative `local/path` to the task\'s VAR environment '
978 'variable using os-appropriate pathsep character. Can be specified '
979 'multiple times for the same VAR to add multiple paths.')
980 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400981 '--idempotent', action='store_true', default=False,
982 help='When set, the server will actively try to find a previous task '
983 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800984 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700985 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700986 help='The optional path to a file containing the secret_bytes to use with'
987 'this task.')
maruel681d6802017-01-17 16:56:03 -0800988 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700989 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400990 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800991 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700992 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400993 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800994 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500995 '--raw-cmd', action='store_true', default=False,
996 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700997 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800998 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500999 '--relative-cwd',
1000 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1001 'requires --raw-cmd')
1002 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001003 '--cipd-package', action='append', default=[], metavar='PKG',
1004 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -07001005 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001006 group.add_option(
1007 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -07001008 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001009 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1010 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001011 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001012 help='Email of a service account to run the task as, or literal "bot" '
1013 'string to indicate that the task should use the same account the '
1014 'bot itself is using to authenticate to Swarming. Don\'t use task '
1015 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001016 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001017 '--pool-task-template',
1018 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1019 default='AUTO',
1020 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
1021 'By default, the pool\'s TaskTemplate is automatically selected, '
1022 'according the pool configuration on the server. Choices are: '
1023 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
1024 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001025 '-o', '--output', action='append', default=[], metavar='PATH',
1026 help='A list of files to return in addition to those written to '
1027 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1028 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001029 group.add_option(
1030 '--wait-for-capacity', action='store_true', default=False,
1031 help='Instructs to leave the task PENDING even if there\'s no known bot '
1032 'that could run this task, otherwise the task will be denied with '
1033 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001034 parser.add_option_group(group)
1035
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001036 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001037 group.add_option(
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +00001038 '--priority', type='int', default=200,
maruel681d6802017-01-17 16:56:03 -08001039 help='The lower value, the more important the task is')
1040 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001041 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001042 help='Display name of the task. Defaults to '
1043 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1044 'isolated file is provided, if a hash is provided, it defaults to '
1045 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1046 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001047 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001048 help='Tags to assign to the task.')
1049 group.add_option(
1050 '--user', default='',
1051 help='User associated with the task. Defaults to authenticated user on '
1052 'the server.')
1053 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001054 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001055 help='Seconds to allow the task to be pending for a bot to run before '
1056 'this task request expires.')
1057 group.add_option(
1058 '--deadline', type='int', dest='expiration',
1059 help=optparse.SUPPRESS_HELP)
1060 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001061
1062
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001063def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001064 """Processes trigger options and does preparatory steps.
1065
1066 Returns:
1067 NewTaskRequest instance.
1068 """
maruelaf6b06c2017-06-08 06:26:53 -07001069 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001070 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001071 if args and args[0] == '--':
1072 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001073
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001074 if not options.dimensions:
1075 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001076 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1077 parser.error('--tags must be in the format key:value')
1078 if options.raw_cmd and not args:
1079 parser.error(
1080 'Arguments with --raw-cmd should be passed after -- as command '
1081 'delimiter.')
1082 if options.isolate_server and not options.namespace:
1083 parser.error(
1084 '--namespace must be a valid value when --isolate-server is used')
1085 if not options.isolated and not options.raw_cmd:
1086 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1087
1088 # Isolated
1089 # --isolated is required only if --raw-cmd wasn't provided.
1090 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1091 # preferred server.
1092 isolateserver.process_isolate_server_options(
1093 parser, options, False, not options.raw_cmd)
1094 inputs_ref = None
1095 if options.isolate_server:
1096 inputs_ref = FilesRef(
1097 isolated=options.isolated,
1098 isolatedserver=options.isolate_server,
1099 namespace=options.namespace)
1100
1101 # Command
1102 command = None
1103 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001104 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001105 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001106 if options.relative_cwd:
1107 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1108 if not a.startswith(os.getcwd()):
1109 parser.error(
1110 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001111 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001112 if options.relative_cwd:
1113 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001114 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001115
maruel0a25f6c2017-05-10 10:43:23 -07001116 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001117 cipd_packages = []
1118 for p in options.cipd_package:
1119 split = p.split(':', 2)
1120 if len(split) != 3:
1121 parser.error('CIPD packages must take the form: path:package:version')
1122 cipd_packages.append(CipdPackage(
1123 package_name=split[1],
1124 path=split[0],
1125 version=split[2]))
1126 cipd_input = None
1127 if cipd_packages:
1128 cipd_input = CipdInput(
1129 client_package=None,
1130 packages=cipd_packages,
1131 server=None)
1132
maruel0a25f6c2017-05-10 10:43:23 -07001133 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001134 secret_bytes = None
1135 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001136 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001137 secret_bytes = f.read().encode('base64')
1138
maruel0a25f6c2017-05-10 10:43:23 -07001139 # Named caches
maruel681d6802017-01-17 16:56:03 -08001140 caches = [
1141 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1142 for i in options.named_cache
1143 ]
maruel0a25f6c2017-05-10 10:43:23 -07001144
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001145 env_prefixes = {}
1146 for k, v in options.env_prefix:
1147 env_prefixes.setdefault(k, []).append(v)
1148
Brad Hallf78187a2018-10-19 17:08:55 +00001149 # Get dimensions into the key/value format we can manipulate later.
1150 orig_dims = [
1151 {'key': key, 'value': value} for key, value in options.dimensions]
1152 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1153
1154 # Construct base properties that we will use for all the slices, adding in
1155 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001156 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001157 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001158 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001159 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001160 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001161 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001162 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001163 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001164 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001165 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001166 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001167 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001168 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001169 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001170 outputs=options.output,
1171 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001172
1173 slices = []
1174
1175 # Group the optional dimensions by expiration.
1176 dims_by_exp = {}
1177 for key, value, exp_secs in options.optional_dimensions:
1178 dims_by_exp.setdefault(int(exp_secs), []).append(
1179 {'key': key, 'value': value})
1180
1181 # Create the optional slices with expiration deltas, we fix up the properties
1182 # below.
1183 last_exp = 0
1184 for expiration_secs in sorted(dims_by_exp):
1185 t = TaskSlice(
1186 expiration_secs=expiration_secs - last_exp,
1187 properties=properties,
1188 wait_for_capacity=False)
1189 slices.append(t)
1190 last_exp = expiration_secs
1191
1192 # Add back in the default slice (the last one).
1193 exp = max(int(options.expiration) - last_exp, 60)
1194 base_task_slice = TaskSlice(
1195 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001196 properties=properties,
1197 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001198 slices.append(base_task_slice)
1199
Brad Hall7f463e62018-11-16 16:13:30 +00001200 # Add optional dimensions to the task slices, replacing a dimension that
1201 # has the same key if it is a dimension where repeating isn't valid (otherwise
1202 # we append it). Currently the only dimension we can repeat is "caches"; the
1203 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001204 extra_dims = []
Brad Hall7f463e62018-11-16 16:13:30 +00001205 for i, (_, kvs) in enumerate(sorted(dims_by_exp.iteritems(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001206 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001207 # Replace or append the key/value pairs for this expiration in extra_dims;
1208 # we keep extra_dims around because we are iterating backwards and filling
1209 # in slices with shorter expirations. Dimensions expire as time goes on so
1210 # the slices that expire earlier will generally have more dimensions.
1211 for kv in kvs:
1212 if kv['key'] == 'caches':
1213 extra_dims.append(kv)
1214 else:
1215 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1216 # Then, add all the optional dimensions to the original dimension set, again
1217 # replacing if needed.
1218 for kv in extra_dims:
1219 if kv['key'] == 'caches':
1220 dims.append(kv)
1221 else:
1222 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001223 dims.sort(key=lambda x: (x['key'], x['value']))
1224 slice_properties = properties._replace(dimensions=dims)
1225 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1226
maruel77f720b2015-09-15 12:35:22 -07001227 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001228 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001229 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001230 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001231 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001232 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001233 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001234 user=options.user,
1235 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001236
1237
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001238class TaskOutputStdoutOption(optparse.Option):
1239 """Where to output the each task's console output (stderr/stdout).
1240
1241 The output will be;
1242 none - not be downloaded.
1243 json - stored in summary.json file *only*.
1244 console - shown on stdout *only*.
1245 all - stored in summary.json and shown on stdout.
1246 """
1247
1248 choices = ['all', 'json', 'console', 'none']
1249
1250 def __init__(self, *args, **kw):
1251 optparse.Option.__init__(
1252 self,
1253 *args,
1254 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001255 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001256 help=re.sub('\s\s*', ' ', self.__doc__),
1257 **kw)
1258
1259 def convert_value(self, opt, value):
1260 if value not in self.choices:
1261 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1262 self.get_opt_string(), self.choices, value))
1263 stdout_to = []
1264 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001265 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001266 elif value != 'none':
1267 stdout_to = [value]
1268 return stdout_to
1269
1270
maruel@chromium.org0437a732013-08-27 16:05:52 +00001271def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001272 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001273 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001274 help='Timeout to wait for result, set to -1 for no timeout and get '
1275 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001276 parser.group_logging.add_option(
1277 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001278 parser.group_logging.add_option(
1279 '--print-status-updates', action='store_true',
1280 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001281 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001282 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001283 '--task-summary-json',
1284 metavar='FILE',
1285 help='Dump a summary of task results to this file as json. It contains '
1286 'only shards statuses as know to server directly. Any output files '
1287 'emitted by the task can be collected by using --task-output-dir')
1288 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001289 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001290 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001291 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001292 'directory contains per-shard directory with output files produced '
1293 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001294 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001295 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001296 parser.task_output_group.add_option(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001297 '--filepath-filter',
1298 help='This is regexp filter used to specify downloaded filepath when '
1299 'collecting isolated output.')
1300 parser.task_output_group.add_option(
maruel9531ce02016-04-13 06:11:23 -07001301 '--perf', action='store_true', default=False,
1302 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001303 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001304
1305
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001306def process_collect_options(parser, options):
1307 # Only negative -1 is allowed, disallow other negative values.
1308 if options.timeout != -1 and options.timeout < 0:
1309 parser.error('Invalid --timeout value')
1310
1311
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001312@subcommand.usage('bots...')
1313def CMDbot_delete(parser, args):
1314 """Forcibly deletes bots from the Swarming server."""
1315 parser.add_option(
1316 '-f', '--force', action='store_true',
1317 help='Do not prompt for confirmation')
1318 options, args = parser.parse_args(args)
1319 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001320 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001321
1322 bots = sorted(args)
1323 if not options.force:
1324 print('Delete the following bots?')
1325 for bot in bots:
1326 print(' %s' % bot)
1327 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1328 print('Goodbye.')
1329 return 1
1330
1331 result = 0
1332 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001333 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001334 if net.url_read_json(url, data={}, method='POST') is None:
1335 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001336 result = 1
1337 return result
1338
1339
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001340def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001341 """Returns information about the bots connected to the Swarming server."""
1342 add_filter_options(parser)
1343 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001344 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001345 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001346 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001347 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001348 help='Keep both dead and alive bots')
1349 parser.filter_group.add_option(
1350 '--busy', action='store_true', help='Keep only busy bots')
1351 parser.filter_group.add_option(
1352 '--idle', action='store_true', help='Keep only idle bots')
1353 parser.filter_group.add_option(
1354 '--mp', action='store_true',
1355 help='Keep only Machine Provider managed bots')
1356 parser.filter_group.add_option(
1357 '--non-mp', action='store_true',
1358 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001359 parser.filter_group.add_option(
1360 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001361 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001362 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001363 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001364
1365 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001366 parser.error('Use only one of --keep-dead or --dead-only')
1367 if options.busy and options.idle:
1368 parser.error('Use only one of --busy or --idle')
1369 if options.mp and options.non_mp:
1370 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001371
smut281c3902018-05-30 17:50:05 -07001372 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001373 values = []
1374 if options.dead_only:
1375 values.append(('is_dead', 'TRUE'))
1376 elif options.keep_dead:
1377 values.append(('is_dead', 'NONE'))
1378 else:
1379 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001380
maruelaf6b06c2017-06-08 06:26:53 -07001381 if options.busy:
1382 values.append(('is_busy', 'TRUE'))
1383 elif options.idle:
1384 values.append(('is_busy', 'FALSE'))
1385 else:
1386 values.append(('is_busy', 'NONE'))
1387
1388 if options.mp:
1389 values.append(('is_mp', 'TRUE'))
1390 elif options.non_mp:
1391 values.append(('is_mp', 'FALSE'))
1392 else:
1393 values.append(('is_mp', 'NONE'))
1394
1395 for key, value in options.dimensions:
1396 values.append(('dimensions', '%s:%s' % (key, value)))
1397 url += urllib.urlencode(values)
1398 try:
1399 data, yielder = get_yielder(url, 0)
1400 bots = data.get('items') or []
1401 for items in yielder():
1402 if items:
1403 bots.extend(items)
1404 except Failure as e:
1405 sys.stderr.write('\n%s\n' % e)
1406 return 1
maruel77f720b2015-09-15 12:35:22 -07001407 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001408 print bot['bot_id']
1409 if not options.bare:
1410 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1411 print ' %s' % json.dumps(dimensions, sort_keys=True)
1412 if bot.get('task_id'):
1413 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001414 return 0
1415
1416
maruelfd0a90c2016-06-10 11:51:10 -07001417@subcommand.usage('task_id')
1418def CMDcancel(parser, args):
1419 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001420 parser.add_option(
1421 '-k', '--kill-running', action='store_true', default=False,
1422 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001423 options, args = parser.parse_args(args)
1424 if not args:
1425 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001426 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001427 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001428 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001429 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001430 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001431 print('Deleting %s failed. Probably already gone' % task_id)
1432 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001433 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001434 return 0
1435
1436
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001437@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001438def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001439 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001440
1441 The result can be in multiple part if the execution was sharded. It can
1442 potentially have retries.
1443 """
1444 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001445 parser.add_option(
1446 '-j', '--json',
1447 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001448 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001449 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001450 if not args and not options.json:
1451 parser.error('Must specify at least one task id or --json.')
1452 if args and options.json:
1453 parser.error('Only use one of task id or --json.')
1454
1455 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001456 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001457 try:
maruel1ceb3872015-10-14 06:10:44 -07001458 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001459 data = json.load(f)
1460 except (IOError, ValueError):
1461 parser.error('Failed to open %s' % options.json)
1462 try:
1463 tasks = sorted(
1464 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1465 args = [t['task_id'] for t in tasks]
1466 except (KeyError, TypeError):
1467 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001468 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001469 # Take in account all the task slices.
1470 offset = 0
1471 for s in data['request']['task_slices']:
1472 m = (offset + s['properties']['execution_timeout_secs'] +
1473 s['expiration_secs'])
1474 if m > options.timeout:
1475 options.timeout = m
1476 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001477 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001478 else:
1479 valid = frozenset('0123456789abcdef')
1480 if any(not valid.issuperset(task_id) for task_id in args):
1481 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001482
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001483 try:
1484 return collect(
1485 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001486 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001487 options.timeout,
1488 options.decorate,
1489 options.print_status_updates,
1490 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001491 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001492 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001493 options.perf,
1494 options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001495 except Failure:
1496 on_error.report(None)
1497 return 1
1498
1499
maruel77f720b2015-09-15 12:35:22 -07001500@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001501def CMDpost(parser, args):
1502 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1503
1504 Input data must be sent to stdin, result is printed to stdout.
1505
1506 If HTTP response code >= 400, returns non-zero.
1507 """
1508 options, args = parser.parse_args(args)
1509 if len(args) != 1:
1510 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001511 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001512 data = sys.stdin.read()
1513 try:
1514 resp = net.url_read(url, data=data, method='POST')
1515 except net.TimeoutError:
1516 sys.stderr.write('Timeout!\n')
1517 return 1
1518 if not resp:
1519 sys.stderr.write('No response!\n')
1520 return 1
1521 sys.stdout.write(resp)
1522 return 0
1523
1524
1525@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001526def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001527 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1528 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001529
1530 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001531 Raw task request and results:
1532 swarming.py query -S server-url.com task/123456/request
1533 swarming.py query -S server-url.com task/123456/result
1534
maruel77f720b2015-09-15 12:35:22 -07001535 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001536 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001537
maruelaf6b06c2017-06-08 06:26:53 -07001538 Listing last 10 tasks on a specific bot named 'bot1':
1539 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001540
maruelaf6b06c2017-06-08 06:26:53 -07001541 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001542 quoting is important!:
1543 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001544 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001545 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001546 parser.add_option(
1547 '-L', '--limit', type='int', default=200,
1548 help='Limit to enforce on limitless items (like number of tasks); '
1549 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001550 parser.add_option(
1551 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001552 parser.add_option(
1553 '--progress', action='store_true',
1554 help='Prints a dot at each request to show progress')
1555 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001556 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001557 parser.error(
1558 'Must specify only method name and optionally query args properly '
1559 'escaped.')
smut281c3902018-05-30 17:50:05 -07001560 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001561 try:
1562 data, yielder = get_yielder(base_url, options.limit)
1563 for items in yielder():
1564 if items:
1565 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001566 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001567 sys.stderr.write('.')
1568 sys.stderr.flush()
1569 except Failure as e:
1570 sys.stderr.write('\n%s\n' % e)
1571 return 1
maruel77f720b2015-09-15 12:35:22 -07001572 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001573 sys.stderr.write('\n')
1574 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001575 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001576 options.json = unicode(os.path.abspath(options.json))
1577 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001578 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001579 try:
maruel77f720b2015-09-15 12:35:22 -07001580 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001581 sys.stdout.write('\n')
1582 except IOError:
1583 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001584 return 0
1585
1586
maruel77f720b2015-09-15 12:35:22 -07001587def CMDquery_list(parser, args):
1588 """Returns list of all the Swarming APIs that can be used with command
1589 'query'.
1590 """
1591 parser.add_option(
1592 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1593 options, args = parser.parse_args(args)
1594 if args:
1595 parser.error('No argument allowed.')
1596
1597 try:
1598 apis = endpoints_api_discovery_apis(options.swarming)
1599 except APIError as e:
1600 parser.error(str(e))
1601 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001602 options.json = unicode(os.path.abspath(options.json))
1603 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001604 json.dump(apis, f)
1605 else:
1606 help_url = (
1607 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1608 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001609 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1610 if i:
1611 print('')
maruel77f720b2015-09-15 12:35:22 -07001612 print api_id
maruel11e31af2017-02-15 07:30:50 -08001613 print ' ' + api['description'].strip()
1614 if 'resources' in api:
1615 # Old.
1616 for j, (resource_name, resource) in enumerate(
1617 sorted(api['resources'].iteritems())):
1618 if j:
1619 print('')
1620 for method_name, method in sorted(resource['methods'].iteritems()):
1621 # Only list the GET ones.
1622 if method['httpMethod'] != 'GET':
1623 continue
1624 print '- %s.%s: %s' % (
1625 resource_name, method_name, method['path'])
1626 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001627 ' ' + l for l in textwrap.wrap(
1628 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001629 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1630 else:
1631 # New.
1632 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001633 # Only list the GET ones.
1634 if method['httpMethod'] != 'GET':
1635 continue
maruel11e31af2017-02-15 07:30:50 -08001636 print '- %s: %s' % (method['id'], method['path'])
1637 print('\n'.join(
1638 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001639 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1640 return 0
1641
1642
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001643@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001644def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001645 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001646
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001647 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001648 """
1649 add_trigger_options(parser)
1650 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001651 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001652 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001653 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001654 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001655 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001656 tasks = trigger_task_shards(
1657 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001658 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001659 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001660 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001661 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001662 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001663 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001664 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001665 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001666 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001667 task_ids = [
1668 t['task_id']
1669 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1670 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001671 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001672 offset = 0
1673 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001674 m = (offset + s.properties.execution_timeout_secs +
1675 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001676 if m > options.timeout:
1677 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001678 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001679 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001680 try:
1681 return collect(
1682 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001683 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001684 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001685 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001686 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001687 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001688 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001689 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001690 options.perf,
1691 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001692 except Failure:
1693 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001694 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001695
1696
maruel18122c62015-10-23 06:31:23 -07001697@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001698def CMDreproduce(parser, args):
1699 """Runs a task locally that was triggered on the server.
1700
1701 This running locally the same commands that have been run on the bot. The data
1702 downloaded will be in a subdirectory named 'work' of the current working
1703 directory.
maruel18122c62015-10-23 06:31:23 -07001704
1705 You can pass further additional arguments to the target command by passing
1706 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001707 """
maruelc070e672016-02-22 17:32:57 -08001708 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001709 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001710 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001711 parser.add_option(
1712 '--work', metavar='DIR', default='work',
1713 help='Directory to map the task input files into')
1714 parser.add_option(
1715 '--cache', metavar='DIR', default='cache',
1716 help='Directory that contains the input cache')
1717 parser.add_option(
1718 '--leak', action='store_true',
1719 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001720 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001721 extra_args = []
1722 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001723 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001724 if len(args) > 1:
1725 if args[1] == '--':
1726 if len(args) > 2:
1727 extra_args = args[2:]
1728 else:
1729 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001730
smut281c3902018-05-30 17:50:05 -07001731 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001732 request = net.url_read_json(url)
1733 if not request:
1734 print >> sys.stderr, 'Failed to retrieve request data for the task'
1735 return 1
1736
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001737 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001738 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001739 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001740 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001741 cachedir = unicode(os.path.abspath('cipd_cache'))
1742 if not fs.exists(cachedir):
1743 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001744
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001745 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001746 env = os.environ.copy()
1747 env['SWARMING_BOT_ID'] = 'reproduce'
1748 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001749 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001750 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001751 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001752 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001753 if not i['value']:
1754 env.pop(key, None)
1755 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001756 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001757
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001758 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001759 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001760 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001761 for i in env_prefixes:
1762 key = i['key']
1763 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001764 cur = env.get(key)
1765 if cur:
1766 paths.append(cur)
1767 env[key] = os.path.pathsep.join(paths)
1768
iannucci31ab9192017-05-02 19:11:56 -07001769 command = []
nodir152cba62016-05-12 16:08:56 -07001770 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001771 # Create the tree.
1772 with isolateserver.get_storage(
1773 properties['inputs_ref']['isolatedserver'],
1774 properties['inputs_ref']['namespace']) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001775 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1776 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1777 # leak.
1778 policies = local_caching.CachePolicies(0, 0, 0, 0)
1779 algo = isolated_format.get_hash_algo(
1780 properties['inputs_ref']['namespace'])
1781 cache = local_caching.DiskContentAddressedCache(
1782 unicode(os.path.abspath(options.cache)), policies, algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001783 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001784 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001785 command = bundle.command
1786 if bundle.relative_cwd:
1787 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001788 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001789
1790 if properties.get('command'):
1791 command.extend(properties['command'])
1792
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001793 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001794 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001795 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001796 new_command = run_isolated.process_command(command, 'invalid', None)
1797 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001798 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001799 else:
1800 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001801 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001802 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001803 command, options.output, None)
1804 if not os.path.isdir(options.output):
1805 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001806 command = new_command
1807 file_path.ensure_command_has_abs_path(command, workdir)
1808
1809 if properties.get('cipd_input'):
1810 ci = properties['cipd_input']
1811 cp = ci['client_package']
1812 client_manager = cipd.get_client(
1813 ci['server'], cp['package_name'], cp['version'], cachedir)
1814
1815 with client_manager as client:
1816 by_path = collections.defaultdict(list)
1817 for pkg in ci['packages']:
1818 path = pkg['path']
1819 # cipd deals with 'root' as ''
1820 if path == '.':
1821 path = ''
1822 by_path[path].append((pkg['package_name'], pkg['version']))
1823 client.ensure(workdir, by_path, cache_dir=cachedir)
1824
maruel77f720b2015-09-15 12:35:22 -07001825 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001826 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001827 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001828 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001829 print >> sys.stderr, str(e)
1830 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001831 finally:
1832 # Do not delete options.cache.
1833 if not options.leak:
1834 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001835
1836
maruel0eb1d1b2015-10-02 14:48:21 -07001837@subcommand.usage('bot_id')
1838def CMDterminate(parser, args):
1839 """Tells a bot to gracefully shut itself down as soon as it can.
1840
1841 This is done by completing whatever current task there is then exiting the bot
1842 process.
1843 """
1844 parser.add_option(
1845 '--wait', action='store_true', help='Wait for the bot to terminate')
1846 options, args = parser.parse_args(args)
1847 if len(args) != 1:
1848 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001849 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001850 request = net.url_read_json(url, data={})
1851 if not request:
1852 print >> sys.stderr, 'Failed to ask for termination'
1853 return 1
1854 if options.wait:
1855 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001856 options.swarming,
1857 [request['task_id']],
1858 0.,
1859 False,
1860 False,
1861 None,
1862 None,
1863 [],
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001864 False,
1865 None)
maruelbfc5f872017-06-10 16:43:17 -07001866 else:
1867 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001868 return 0
1869
1870
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001871@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001872def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001873 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001874
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001875 Passes all extra arguments provided after '--' as additional command line
1876 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001877 """
1878 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001879 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001880 parser.add_option(
1881 '--dump-json',
1882 metavar='FILE',
1883 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001884 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001885 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001886 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001887 tasks = trigger_task_shards(
1888 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001889 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001890 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001891 tasks_sorted = sorted(
1892 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001893 if options.dump_json:
1894 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001895 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001896 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001897 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001898 }
maruel46b015f2015-10-13 18:40:35 -07001899 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001900 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001901 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001902 (options.swarming, options.dump_json))
1903 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001904 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001905 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001906 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1907 print('Or visit:')
1908 for t in tasks_sorted:
1909 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001910 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001911 except Failure:
1912 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001913 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001914
1915
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001916class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001917 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001918 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001919 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001920 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001921 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001922 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001923 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001924 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001925 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001926 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001927
1928 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001929 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001930 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001931 auth.process_auth_options(self, options)
1932 user = self._process_swarming(options)
1933 if hasattr(options, 'user') and not options.user:
1934 options.user = user
1935 return options, args
1936
1937 def _process_swarming(self, options):
1938 """Processes the --swarming option and aborts if not specified.
1939
1940 Returns the identity as determined by the server.
1941 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001942 if not options.swarming:
1943 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001944 try:
1945 options.swarming = net.fix_url(options.swarming)
1946 except ValueError as e:
1947 self.error('--swarming %s' % e)
1948 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001949 try:
1950 user = auth.ensure_logged_in(options.swarming)
1951 except ValueError as e:
1952 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001953 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001954
1955
1956def main(args):
1957 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001958 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001959
1960
1961if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001962 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001963 fix_encoding.fix_encoding()
1964 tools.disable_buffering()
1965 colorama.init()
1966 sys.exit(main(sys.argv[1:]))