blob: ea44293c70bb57dc58c5464c73d0afcd56b3df84 [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'.
285STATUS_UPDATE_INTERVAL = 15 * 60.
286
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
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000339 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700340 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
341
342 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700343 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700344 shard_count: expected number of task shards.
345 """
maruel12e30012015-10-09 11:55:35 -0700346 self.task_output_dir = (
347 unicode(os.path.abspath(task_output_dir))
348 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000349 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700350 self.shard_count = shard_count
351
352 self._lock = threading.Lock()
353 self._per_shard_results = {}
354 self._storage = None
355
nodire5028a92016-04-29 14:38:21 -0700356 if self.task_output_dir:
357 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700358
Vadim Shtayurab450c602014-05-12 19:23:25 -0700359 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360 """Stores results of a single task shard, fetches output files if necessary.
361
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400362 Modifies |result| in place.
363
maruel77f720b2015-09-15 12:35:22 -0700364 shard_index is 0-based.
365
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700366 Called concurrently from multiple threads.
367 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700369 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370 if shard_index < 0 or shard_index >= self.shard_count:
371 logging.warning(
372 'Shard index %d is outside of expected range: [0; %d]',
373 shard_index, self.shard_count - 1)
374 return
375
maruel77f720b2015-09-15 12:35:22 -0700376 if result.get('outputs_ref'):
377 ref = result['outputs_ref']
378 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
379 ref['isolatedserver'],
380 urllib.urlencode(
381 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400382
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700383 # Store result dict of that shard, ignore results we've already seen.
384 with self._lock:
385 if shard_index in self._per_shard_results:
386 logging.warning('Ignoring duplicate shard index %d', shard_index)
387 return
388 self._per_shard_results[shard_index] = result
389
390 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700391 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400392 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700393 result['outputs_ref']['isolatedserver'],
394 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400395 if storage:
396 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400397 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
398 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400399 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700400 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400401 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400402 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700403 os.path.join(self.task_output_dir, str(shard_index)),
404 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700405
406 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700407 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700408 with self._lock:
409 # Write an array of shard results with None for missing shards.
410 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700411 'shards': [
412 self._per_shard_results.get(i) for i in xrange(self.shard_count)
413 ],
414 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000415
416 # Don't store stdout in the summary if not requested too.
417 if "json" not in self.task_output_stdout:
418 for shard_json in summary['shards']:
419 if not shard_json:
420 continue
421 if "output" in shard_json:
422 del shard_json["output"]
423 if "outputs" in shard_json:
424 del shard_json["outputs"]
425
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700426 # Write summary.json to task_output_dir as well.
427 if self.task_output_dir:
428 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700429 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700430 summary,
431 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700432 if self._storage:
433 self._storage.close()
434 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700435 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700436
437 def _get_storage(self, isolate_server, namespace):
438 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700439 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700440 with self._lock:
441 if not self._storage:
442 self._storage = isolateserver.get_storage(isolate_server, namespace)
443 else:
444 # Shards must all use exact same isolate server and namespace.
445 if self._storage.location != isolate_server:
446 logging.error(
447 'Task shards are using multiple isolate servers: %s and %s',
448 self._storage.location, isolate_server)
449 return None
450 if self._storage.namespace != namespace:
451 logging.error(
452 'Task shards are using multiple namespaces: %s and %s',
453 self._storage.namespace, namespace)
454 return None
455 return self._storage
456
457
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500458def now():
459 """Exists so it can be mocked easily."""
460 return time.time()
461
462
maruel77f720b2015-09-15 12:35:22 -0700463def parse_time(value):
464 """Converts serialized time from the API to datetime.datetime."""
465 # When microseconds are 0, the '.123456' suffix is elided. This means the
466 # serialized format is not consistent, which confuses the hell out of python.
467 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
468 try:
469 return datetime.datetime.strptime(value, fmt)
470 except ValueError:
471 pass
472 raise ValueError('Failed to parse %s' % value)
473
474
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700475def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700476 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000477 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400478 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700479
Vadim Shtayurab450c602014-05-12 19:23:25 -0700480 Returns:
481 <result dict> on success.
482 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700483 """
maruel71c61c82016-02-22 06:52:05 -0800484 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700485 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700486 if include_perf:
487 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700488 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700489 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400490 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700491 attempt = 0
492
493 while not should_stop.is_set():
494 attempt += 1
495
496 # Waiting for too long -> give up.
497 current_time = now()
498 if deadline and current_time >= deadline:
499 logging.error('retrieve_results(%s) timed out on attempt %d',
500 base_url, attempt)
501 return None
502
503 # Do not spin too fast. Spin faster at the beginning though.
504 # Start with 1 sec delay and for each 30 sec of waiting add another second
505 # of delay, until hitting 15 sec ceiling.
506 if attempt > 1:
507 max_delay = min(15, 1 + (current_time - started) / 30.0)
508 delay = min(max_delay, deadline - current_time) if deadline else max_delay
509 if delay > 0:
510 logging.debug('Waiting %.1f sec before retrying', delay)
511 should_stop.wait(delay)
512 if should_stop.is_set():
513 return None
514
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400515 # Disable internal retries in net.url_read_json, since we are doing retries
516 # ourselves.
517 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700518 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
519 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400520 # Retry on 500s only if no timeout is specified.
521 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400522 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400523 if timeout == -1:
524 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400525 continue
maruel77f720b2015-09-15 12:35:22 -0700526
maruelbf53e042015-12-01 15:00:51 -0800527 if result.get('error'):
528 # An error occurred.
529 if result['error'].get('errors'):
530 for err in result['error']['errors']:
531 logging.warning(
532 'Error while reading task: %s; %s',
533 err.get('message'), err.get('debugInfo'))
534 elif result['error'].get('message'):
535 logging.warning(
536 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400537 if timeout == -1:
538 return result
maruelbf53e042015-12-01 15:00:51 -0800539 continue
540
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400541 # When timeout == -1, always return on first attempt. 500s are already
542 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000543 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000544 if fetch_stdout:
545 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700546 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700547 # Record the result, try to fetch attached output files (if any).
548 if output_collector:
549 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700550 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700551 if result.get('internal_failure'):
552 logging.error('Internal error!')
553 elif result['state'] == 'BOT_DIED':
554 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700555 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000556
557
maruel77f720b2015-09-15 12:35:22 -0700558def convert_to_old_format(result):
559 """Converts the task result data from Endpoints API format to old API format
560 for compatibility.
561
562 This goes into the file generated as --task-summary-json.
563 """
564 # Sets default.
565 result.setdefault('abandoned_ts', None)
566 result.setdefault('bot_id', None)
567 result.setdefault('bot_version', None)
568 result.setdefault('children_task_ids', [])
569 result.setdefault('completed_ts', None)
570 result.setdefault('cost_saved_usd', None)
571 result.setdefault('costs_usd', None)
572 result.setdefault('deduped_from', None)
573 result.setdefault('name', None)
574 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700575 result.setdefault('server_versions', None)
576 result.setdefault('started_ts', None)
577 result.setdefault('tags', None)
578 result.setdefault('user', None)
579
580 # Convertion back to old API.
581 duration = result.pop('duration', None)
582 result['durations'] = [duration] if duration else []
583 exit_code = result.pop('exit_code', None)
584 result['exit_codes'] = [int(exit_code)] if exit_code else []
585 result['id'] = result.pop('task_id')
586 result['isolated_out'] = result.get('outputs_ref', None)
587 output = result.pop('output', None)
588 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700589 # server_version
590 # Endpoints result 'state' as string. For compatibility with old code, convert
591 # to int.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000592 result['state'] = TaskState.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700593 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700594 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700595 if 'bot_dimensions' in result:
596 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700597 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700598 }
599 else:
600 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700601
602
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700603def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400604 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000605 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500606 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000607
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700608 Duplicate shards are ignored. Shards are yielded in order of completion.
609 Timed out shards are NOT yielded at all. Caller can compare number of yielded
610 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000611
612 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500613 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 +0000614 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500615
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700616 output_collector is an optional instance of TaskOutputCollector that will be
617 used to fetch files produced by a task from isolate server to the local disk.
618
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500619 Yields:
620 (index, result). In particular, 'result' is defined as the
621 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000622 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000623 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400624 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700625 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700626 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700627
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
629 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700630 # Adds a task to the thread pool to call 'retrieve_results' and return
631 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400632 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000633 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000635 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400636 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000637 task_id, timeout, should_stop, output_collector, include_perf,
638 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639
640 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 for shard_index, task_id in enumerate(task_ids):
642 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643
644 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400645 shards_remaining = range(len(task_ids))
646 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700647 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700648 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 shard_index, result = results_channel.pull(
651 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700652 except threading_utils.TaskChannel.Timeout:
653 if print_status_updates:
654 print(
655 'Waiting for results from the following shards: %s' %
656 ', '.join(map(str, shards_remaining)))
657 sys.stdout.flush()
658 continue
659 except Exception:
660 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700661
662 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000664 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500665 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000666 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700667
Vadim Shtayurab450c602014-05-12 19:23:25 -0700668 # Yield back results to the caller.
669 assert shard_index in shards_remaining
670 shards_remaining.remove(shard_index)
671 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700672
maruel@chromium.org0437a732013-08-27 16:05:52 +0000673 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 should_stop.set()
676
677
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000678def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700680 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400681 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700682 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
683 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400684 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
685 metadata.get('abandoned_ts')):
686 pending = '%.1fs' % (
687 parse_time(metadata['abandoned_ts']) -
688 parse_time(metadata['created_ts'])
689 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400690 else:
691 pending = 'N/A'
692
maruel77f720b2015-09-15 12:35:22 -0700693 if metadata.get('duration') is not None:
694 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400695 else:
696 duration = 'N/A'
697
maruel77f720b2015-09-15 12:35:22 -0700698 if metadata.get('exit_code') is not None:
699 # Integers are encoded as string to not loose precision.
700 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400701 else:
702 exit_code = 'N/A'
703
704 bot_id = metadata.get('bot_id') or 'N/A'
705
maruel77f720b2015-09-15 12:35:22 -0700706 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400707 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000708 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400709 if metadata.get('state') == 'CANCELED':
710 tag_footer2 = ' Pending: %s CANCELED' % pending
711 elif metadata.get('state') == 'EXPIRED':
712 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400713 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400714 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
715 pending, duration, bot_id, exit_code, metadata['state'])
716 else:
717 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
718 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400719
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000720 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
721 dash_pad = '+-%s-+' % ('-' * tag_len)
722 tag_header = '| %s |' % tag_header.ljust(tag_len)
723 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
724 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400725
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000726 if include_stdout:
727 return '\n'.join([
728 dash_pad,
729 tag_header,
730 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400731 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000732 dash_pad,
733 tag_footer1,
734 tag_footer2,
735 dash_pad,
736 ])
737 else:
738 return '\n'.join([
739 dash_pad,
740 tag_header,
741 tag_footer2,
742 dash_pad,
743 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000744
745
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700747 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000748 task_summary_json, task_output_dir, task_output_stdout,
749 include_perf):
maruela5490782015-09-30 10:56:59 -0700750 """Retrieves results of a Swarming task.
751
752 Returns:
753 process exit code that should be returned to the user.
754 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700755 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000756 output_collector = TaskOutputCollector(
757 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700758
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700759 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700760 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400761 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700762 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400763 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400764 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000765 output_collector, include_perf,
766 (len(task_output_stdout) > 0),
767 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700768 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700769
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400770 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700771 shard_exit_code = metadata.get('exit_code')
772 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700773 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700774 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700775 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400776 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700777 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700778
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700779 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000780 s = decorate_shard_output(
781 swarming, index, metadata,
782 "console" in task_output_stdout).encode(
783 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700784 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400785 if len(seen_shards) < len(task_ids):
786 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700787 else:
maruel77f720b2015-09-15 12:35:22 -0700788 print('%s: %s %s' % (
789 metadata.get('bot_id', 'N/A'),
790 metadata['task_id'],
791 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000792 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700793 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400794 if output:
795 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700796 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700797 summary = output_collector.finalize()
798 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700799 # TODO(maruel): Make this optional.
800 for i in summary['shards']:
801 if i:
802 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700803 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700804
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400805 if decorate and total_duration:
806 print('Total duration: %.1fs' % total_duration)
807
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400808 if len(seen_shards) != len(task_ids):
809 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700810 print >> sys.stderr, ('Results from some shards are missing: %s' %
811 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700812 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700813
maruela5490782015-09-30 10:56:59 -0700814 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000815
816
maruel77f720b2015-09-15 12:35:22 -0700817### API management.
818
819
820class APIError(Exception):
821 pass
822
823
824def endpoints_api_discovery_apis(host):
825 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
826 the APIs exposed by a host.
827
828 https://developers.google.com/discovery/v1/reference/apis/list
829 """
maruel380e3262016-08-31 16:10:06 -0700830 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
831 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700832 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
833 if data is None:
834 raise APIError('Failed to discover APIs on %s' % host)
835 out = {}
836 for api in data['items']:
837 if api['id'] == 'discovery:v1':
838 continue
839 # URL is of the following form:
840 # url = host + (
841 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
842 api_data = net.url_read_json(api['discoveryRestUrl'])
843 if api_data is None:
844 raise APIError('Failed to discover %s on %s' % (api['id'], host))
845 out[api['id']] = api_data
846 return out
847
848
maruelaf6b06c2017-06-08 06:26:53 -0700849def get_yielder(base_url, limit):
850 """Returns the first query and a function that yields following items."""
851 CHUNK_SIZE = 250
852
853 url = base_url
854 if limit:
855 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
856 data = net.url_read_json(url)
857 if data is None:
858 # TODO(maruel): Do basic diagnostic.
859 raise Failure('Failed to access %s' % url)
860 org_cursor = data.pop('cursor', None)
861 org_total = len(data.get('items') or [])
862 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
863 if not org_cursor or not org_total:
864 # This is not an iterable resource.
865 return data, lambda: []
866
867 def yielder():
868 cursor = org_cursor
869 total = org_total
870 # Some items support cursors. Try to get automatically if cursors are needed
871 # by looking at the 'cursor' items.
872 while cursor and (not limit or total < limit):
873 merge_char = '&' if '?' in base_url else '?'
874 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
875 if limit:
876 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
877 new = net.url_read_json(url)
878 if new is None:
879 raise Failure('Failed to access %s' % url)
880 cursor = new.get('cursor')
881 new_items = new.get('items')
882 nb_items = len(new_items or [])
883 total += nb_items
884 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
885 yield new_items
886
887 return data, yielder
888
889
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500890### Commands.
891
892
893def abort_task(_swarming, _manifest):
894 """Given a task manifest that was triggered, aborts its execution."""
895 # TODO(vadimsh): No supported by the server yet.
896
897
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400898def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800899 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500900 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500901 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500902 dest='dimensions', metavar='FOO bar',
903 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000904 parser.filter_group.add_option(
905 '--optional-dimension', default=[], action='append', nargs=3,
906 dest='optional_dimensions', metavar='key value expiration',
907 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500908 parser.add_option_group(parser.filter_group)
909
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400910
Brad Hallf78187a2018-10-19 17:08:55 +0000911def _validate_filter_option(parser, key, value, expiration, argname):
912 if ':' in key:
913 parser.error('%s key cannot contain ":"' % argname)
914 if key.strip() != key:
915 parser.error('%s key has whitespace' % argname)
916 if not key:
917 parser.error('%s key is empty' % argname)
918
919 if value.strip() != value:
920 parser.error('%s value has whitespace' % argname)
921 if not value:
922 parser.error('%s value is empty' % argname)
923
924 if expiration is not None:
925 try:
926 expiration = int(expiration)
927 except ValueError:
928 parser.error('%s expiration is not an integer' % argname)
929 if expiration <= 0:
930 parser.error('%s expiration should be positive' % argname)
931 if expiration % 60 != 0:
932 parser.error('%s expiration is not divisible by 60' % argname)
933
934
maruelaf6b06c2017-06-08 06:26:53 -0700935def process_filter_options(parser, options):
936 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000937 _validate_filter_option(parser, key, value, None, 'dimension')
938 for key, value, exp in options.optional_dimensions:
939 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700940 options.dimensions.sort()
941
942
Vadim Shtayurab450c602014-05-12 19:23:25 -0700943def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400944 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700945 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700946 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700947 help='Number of shards to trigger and collect.')
948 parser.add_option_group(parser.sharding_group)
949
950
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400951def add_trigger_options(parser):
952 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500953 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400954 add_filter_options(parser)
955
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400956 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800957 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700958 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500959 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800960 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500961 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700962 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800963 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800964 '--env-prefix', default=[], action='append', nargs=2,
965 metavar='VAR local/path',
966 help='Prepend task-relative `local/path` to the task\'s VAR environment '
967 'variable using os-appropriate pathsep character. Can be specified '
968 'multiple times for the same VAR to add multiple paths.')
969 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400970 '--idempotent', action='store_true', default=False,
971 help='When set, the server will actively try to find a previous task '
972 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800973 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700974 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700975 help='The optional path to a file containing the secret_bytes to use with'
976 'this task.')
maruel681d6802017-01-17 16:56:03 -0800977 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700978 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400979 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800980 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700981 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400982 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800983 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500984 '--raw-cmd', action='store_true', default=False,
985 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700986 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800987 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500988 '--relative-cwd',
989 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
990 'requires --raw-cmd')
991 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700992 '--cipd-package', action='append', default=[], metavar='PKG',
993 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700994 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800995 group.add_option(
996 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700997 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800998 help='"<name> <relpath>" items to keep a persistent bot managed cache')
999 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001000 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001001 help='Email of a service account to run the task as, or literal "bot" '
1002 'string to indicate that the task should use the same account the '
1003 'bot itself is using to authenticate to Swarming. Don\'t use task '
1004 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001005 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001006 '--pool-task-template',
1007 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1008 default='AUTO',
1009 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
1010 'By default, the pool\'s TaskTemplate is automatically selected, '
1011 'according the pool configuration on the server. Choices are: '
1012 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
1013 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001014 '-o', '--output', action='append', default=[], metavar='PATH',
1015 help='A list of files to return in addition to those written to '
1016 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1017 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001018 group.add_option(
1019 '--wait-for-capacity', action='store_true', default=False,
1020 help='Instructs to leave the task PENDING even if there\'s no known bot '
1021 'that could run this task, otherwise the task will be denied with '
1022 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001023 parser.add_option_group(group)
1024
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001025 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001026 group.add_option(
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +00001027 '--priority', type='int', default=200,
maruel681d6802017-01-17 16:56:03 -08001028 help='The lower value, the more important the task is')
1029 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001030 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001031 help='Display name of the task. Defaults to '
1032 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1033 'isolated file is provided, if a hash is provided, it defaults to '
1034 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1035 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001036 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001037 help='Tags to assign to the task.')
1038 group.add_option(
1039 '--user', default='',
1040 help='User associated with the task. Defaults to authenticated user on '
1041 'the server.')
1042 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001043 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001044 help='Seconds to allow the task to be pending for a bot to run before '
1045 'this task request expires.')
1046 group.add_option(
1047 '--deadline', type='int', dest='expiration',
1048 help=optparse.SUPPRESS_HELP)
1049 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001050
1051
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001052def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001053 """Processes trigger options and does preparatory steps.
1054
1055 Returns:
1056 NewTaskRequest instance.
1057 """
maruelaf6b06c2017-06-08 06:26:53 -07001058 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001059 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001060 if args and args[0] == '--':
1061 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001062
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001063 if not options.dimensions:
1064 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001065 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1066 parser.error('--tags must be in the format key:value')
1067 if options.raw_cmd and not args:
1068 parser.error(
1069 'Arguments with --raw-cmd should be passed after -- as command '
1070 'delimiter.')
1071 if options.isolate_server and not options.namespace:
1072 parser.error(
1073 '--namespace must be a valid value when --isolate-server is used')
1074 if not options.isolated and not options.raw_cmd:
1075 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1076
1077 # Isolated
1078 # --isolated is required only if --raw-cmd wasn't provided.
1079 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1080 # preferred server.
1081 isolateserver.process_isolate_server_options(
1082 parser, options, False, not options.raw_cmd)
1083 inputs_ref = None
1084 if options.isolate_server:
1085 inputs_ref = FilesRef(
1086 isolated=options.isolated,
1087 isolatedserver=options.isolate_server,
1088 namespace=options.namespace)
1089
1090 # Command
1091 command = None
1092 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001093 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001094 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001095 if options.relative_cwd:
1096 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1097 if not a.startswith(os.getcwd()):
1098 parser.error(
1099 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001100 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001101 if options.relative_cwd:
1102 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001103 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001104
maruel0a25f6c2017-05-10 10:43:23 -07001105 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001106 cipd_packages = []
1107 for p in options.cipd_package:
1108 split = p.split(':', 2)
1109 if len(split) != 3:
1110 parser.error('CIPD packages must take the form: path:package:version')
1111 cipd_packages.append(CipdPackage(
1112 package_name=split[1],
1113 path=split[0],
1114 version=split[2]))
1115 cipd_input = None
1116 if cipd_packages:
1117 cipd_input = CipdInput(
1118 client_package=None,
1119 packages=cipd_packages,
1120 server=None)
1121
maruel0a25f6c2017-05-10 10:43:23 -07001122 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001123 secret_bytes = None
1124 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001125 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001126 secret_bytes = f.read().encode('base64')
1127
maruel0a25f6c2017-05-10 10:43:23 -07001128 # Named caches
maruel681d6802017-01-17 16:56:03 -08001129 caches = [
1130 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1131 for i in options.named_cache
1132 ]
maruel0a25f6c2017-05-10 10:43:23 -07001133
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001134 env_prefixes = {}
1135 for k, v in options.env_prefix:
1136 env_prefixes.setdefault(k, []).append(v)
1137
Brad Hallf78187a2018-10-19 17:08:55 +00001138 # Get dimensions into the key/value format we can manipulate later.
1139 orig_dims = [
1140 {'key': key, 'value': value} for key, value in options.dimensions]
1141 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1142
1143 # Construct base properties that we will use for all the slices, adding in
1144 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001145 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001146 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001147 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001148 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001149 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001150 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001151 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001152 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001153 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001154 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001155 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001156 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001157 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001158 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001159 outputs=options.output,
1160 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001161
1162 slices = []
1163
1164 # Group the optional dimensions by expiration.
1165 dims_by_exp = {}
1166 for key, value, exp_secs in options.optional_dimensions:
1167 dims_by_exp.setdefault(int(exp_secs), []).append(
1168 {'key': key, 'value': value})
1169
1170 # Create the optional slices with expiration deltas, we fix up the properties
1171 # below.
1172 last_exp = 0
1173 for expiration_secs in sorted(dims_by_exp):
1174 t = TaskSlice(
1175 expiration_secs=expiration_secs - last_exp,
1176 properties=properties,
1177 wait_for_capacity=False)
1178 slices.append(t)
1179 last_exp = expiration_secs
1180
1181 # Add back in the default slice (the last one).
1182 exp = max(int(options.expiration) - last_exp, 60)
1183 base_task_slice = TaskSlice(
1184 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001185 properties=properties,
1186 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001187 slices.append(base_task_slice)
1188
1189 # Add optional dimensions to the fallback slices.
1190 extra_dims = []
1191 for i, (_, kv) in enumerate(sorted(dims_by_exp.iteritems(), reverse=True)):
1192 extra_dims.extend(kv)
1193 dims = list(orig_dims)
1194 dims.extend(extra_dims)
1195 dims.sort(key=lambda x: (x['key'], x['value']))
1196 slice_properties = properties._replace(dimensions=dims)
1197 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1198
maruel77f720b2015-09-15 12:35:22 -07001199 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001200 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001201 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001202 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001203 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001204 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001205 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001206 user=options.user,
1207 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001208
1209
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001210class TaskOutputStdoutOption(optparse.Option):
1211 """Where to output the each task's console output (stderr/stdout).
1212
1213 The output will be;
1214 none - not be downloaded.
1215 json - stored in summary.json file *only*.
1216 console - shown on stdout *only*.
1217 all - stored in summary.json and shown on stdout.
1218 """
1219
1220 choices = ['all', 'json', 'console', 'none']
1221
1222 def __init__(self, *args, **kw):
1223 optparse.Option.__init__(
1224 self,
1225 *args,
1226 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001227 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001228 help=re.sub('\s\s*', ' ', self.__doc__),
1229 **kw)
1230
1231 def convert_value(self, opt, value):
1232 if value not in self.choices:
1233 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1234 self.get_opt_string(), self.choices, value))
1235 stdout_to = []
1236 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001237 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001238 elif value != 'none':
1239 stdout_to = [value]
1240 return stdout_to
1241
1242
maruel@chromium.org0437a732013-08-27 16:05:52 +00001243def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001244 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001245 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001246 help='Timeout to wait for result, set to -1 for no timeout and get '
1247 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001248 parser.group_logging.add_option(
1249 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001250 parser.group_logging.add_option(
1251 '--print-status-updates', action='store_true',
1252 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001253 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001254 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001255 '--task-summary-json',
1256 metavar='FILE',
1257 help='Dump a summary of task results to this file as json. It contains '
1258 'only shards statuses as know to server directly. Any output files '
1259 'emitted by the task can be collected by using --task-output-dir')
1260 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001261 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001262 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001263 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001264 'directory contains per-shard directory with output files produced '
1265 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001266 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001267 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001268 parser.task_output_group.add_option(
1269 '--perf', action='store_true', default=False,
1270 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001271 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001272
1273
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001274def process_collect_options(parser, options):
1275 # Only negative -1 is allowed, disallow other negative values.
1276 if options.timeout != -1 and options.timeout < 0:
1277 parser.error('Invalid --timeout value')
1278
1279
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001280@subcommand.usage('bots...')
1281def CMDbot_delete(parser, args):
1282 """Forcibly deletes bots from the Swarming server."""
1283 parser.add_option(
1284 '-f', '--force', action='store_true',
1285 help='Do not prompt for confirmation')
1286 options, args = parser.parse_args(args)
1287 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001288 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001289
1290 bots = sorted(args)
1291 if not options.force:
1292 print('Delete the following bots?')
1293 for bot in bots:
1294 print(' %s' % bot)
1295 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1296 print('Goodbye.')
1297 return 1
1298
1299 result = 0
1300 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001301 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001302 if net.url_read_json(url, data={}, method='POST') is None:
1303 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001304 result = 1
1305 return result
1306
1307
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001308def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001309 """Returns information about the bots connected to the Swarming server."""
1310 add_filter_options(parser)
1311 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001312 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001313 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001314 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001315 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001316 help='Keep both dead and alive bots')
1317 parser.filter_group.add_option(
1318 '--busy', action='store_true', help='Keep only busy bots')
1319 parser.filter_group.add_option(
1320 '--idle', action='store_true', help='Keep only idle bots')
1321 parser.filter_group.add_option(
1322 '--mp', action='store_true',
1323 help='Keep only Machine Provider managed bots')
1324 parser.filter_group.add_option(
1325 '--non-mp', action='store_true',
1326 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001327 parser.filter_group.add_option(
1328 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001329 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001330 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001331 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001332
1333 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001334 parser.error('Use only one of --keep-dead or --dead-only')
1335 if options.busy and options.idle:
1336 parser.error('Use only one of --busy or --idle')
1337 if options.mp and options.non_mp:
1338 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001339
smut281c3902018-05-30 17:50:05 -07001340 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001341 values = []
1342 if options.dead_only:
1343 values.append(('is_dead', 'TRUE'))
1344 elif options.keep_dead:
1345 values.append(('is_dead', 'NONE'))
1346 else:
1347 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001348
maruelaf6b06c2017-06-08 06:26:53 -07001349 if options.busy:
1350 values.append(('is_busy', 'TRUE'))
1351 elif options.idle:
1352 values.append(('is_busy', 'FALSE'))
1353 else:
1354 values.append(('is_busy', 'NONE'))
1355
1356 if options.mp:
1357 values.append(('is_mp', 'TRUE'))
1358 elif options.non_mp:
1359 values.append(('is_mp', 'FALSE'))
1360 else:
1361 values.append(('is_mp', 'NONE'))
1362
1363 for key, value in options.dimensions:
1364 values.append(('dimensions', '%s:%s' % (key, value)))
1365 url += urllib.urlencode(values)
1366 try:
1367 data, yielder = get_yielder(url, 0)
1368 bots = data.get('items') or []
1369 for items in yielder():
1370 if items:
1371 bots.extend(items)
1372 except Failure as e:
1373 sys.stderr.write('\n%s\n' % e)
1374 return 1
maruel77f720b2015-09-15 12:35:22 -07001375 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001376 print bot['bot_id']
1377 if not options.bare:
1378 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1379 print ' %s' % json.dumps(dimensions, sort_keys=True)
1380 if bot.get('task_id'):
1381 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001382 return 0
1383
1384
maruelfd0a90c2016-06-10 11:51:10 -07001385@subcommand.usage('task_id')
1386def CMDcancel(parser, args):
1387 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001388 parser.add_option(
1389 '-k', '--kill-running', action='store_true', default=False,
1390 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001391 options, args = parser.parse_args(args)
1392 if not args:
1393 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001394 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001395 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001396 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001397 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001398 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001399 print('Deleting %s failed. Probably already gone' % task_id)
1400 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001401 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001402 return 0
1403
1404
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001405@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001406def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001407 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001408
1409 The result can be in multiple part if the execution was sharded. It can
1410 potentially have retries.
1411 """
1412 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001413 parser.add_option(
1414 '-j', '--json',
1415 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001416 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001417 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001418 if not args and not options.json:
1419 parser.error('Must specify at least one task id or --json.')
1420 if args and options.json:
1421 parser.error('Only use one of task id or --json.')
1422
1423 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001424 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001425 try:
maruel1ceb3872015-10-14 06:10:44 -07001426 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001427 data = json.load(f)
1428 except (IOError, ValueError):
1429 parser.error('Failed to open %s' % options.json)
1430 try:
1431 tasks = sorted(
1432 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1433 args = [t['task_id'] for t in tasks]
1434 except (KeyError, TypeError):
1435 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001436 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001437 # Take in account all the task slices.
1438 offset = 0
1439 for s in data['request']['task_slices']:
1440 m = (offset + s['properties']['execution_timeout_secs'] +
1441 s['expiration_secs'])
1442 if m > options.timeout:
1443 options.timeout = m
1444 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001445 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001446 else:
1447 valid = frozenset('0123456789abcdef')
1448 if any(not valid.issuperset(task_id) for task_id in args):
1449 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001450
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001451 try:
1452 return collect(
1453 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001454 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001455 options.timeout,
1456 options.decorate,
1457 options.print_status_updates,
1458 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001459 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001460 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001461 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001462 except Failure:
1463 on_error.report(None)
1464 return 1
1465
1466
maruel77f720b2015-09-15 12:35:22 -07001467@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001468def CMDpost(parser, args):
1469 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1470
1471 Input data must be sent to stdin, result is printed to stdout.
1472
1473 If HTTP response code >= 400, returns non-zero.
1474 """
1475 options, args = parser.parse_args(args)
1476 if len(args) != 1:
1477 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001478 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001479 data = sys.stdin.read()
1480 try:
1481 resp = net.url_read(url, data=data, method='POST')
1482 except net.TimeoutError:
1483 sys.stderr.write('Timeout!\n')
1484 return 1
1485 if not resp:
1486 sys.stderr.write('No response!\n')
1487 return 1
1488 sys.stdout.write(resp)
1489 return 0
1490
1491
1492@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001493def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001494 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1495 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001496
1497 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001498 Raw task request and results:
1499 swarming.py query -S server-url.com task/123456/request
1500 swarming.py query -S server-url.com task/123456/result
1501
maruel77f720b2015-09-15 12:35:22 -07001502 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001503 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001504
maruelaf6b06c2017-06-08 06:26:53 -07001505 Listing last 10 tasks on a specific bot named 'bot1':
1506 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001507
maruelaf6b06c2017-06-08 06:26:53 -07001508 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001509 quoting is important!:
1510 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001511 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001512 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001513 parser.add_option(
1514 '-L', '--limit', type='int', default=200,
1515 help='Limit to enforce on limitless items (like number of tasks); '
1516 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001517 parser.add_option(
1518 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001519 parser.add_option(
1520 '--progress', action='store_true',
1521 help='Prints a dot at each request to show progress')
1522 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001523 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001524 parser.error(
1525 'Must specify only method name and optionally query args properly '
1526 'escaped.')
smut281c3902018-05-30 17:50:05 -07001527 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001528 try:
1529 data, yielder = get_yielder(base_url, options.limit)
1530 for items in yielder():
1531 if items:
1532 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001533 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001534 sys.stderr.write('.')
1535 sys.stderr.flush()
1536 except Failure as e:
1537 sys.stderr.write('\n%s\n' % e)
1538 return 1
maruel77f720b2015-09-15 12:35:22 -07001539 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001540 sys.stderr.write('\n')
1541 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001542 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001543 options.json = unicode(os.path.abspath(options.json))
1544 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001545 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001546 try:
maruel77f720b2015-09-15 12:35:22 -07001547 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001548 sys.stdout.write('\n')
1549 except IOError:
1550 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001551 return 0
1552
1553
maruel77f720b2015-09-15 12:35:22 -07001554def CMDquery_list(parser, args):
1555 """Returns list of all the Swarming APIs that can be used with command
1556 'query'.
1557 """
1558 parser.add_option(
1559 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1560 options, args = parser.parse_args(args)
1561 if args:
1562 parser.error('No argument allowed.')
1563
1564 try:
1565 apis = endpoints_api_discovery_apis(options.swarming)
1566 except APIError as e:
1567 parser.error(str(e))
1568 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001569 options.json = unicode(os.path.abspath(options.json))
1570 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001571 json.dump(apis, f)
1572 else:
1573 help_url = (
1574 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1575 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001576 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1577 if i:
1578 print('')
maruel77f720b2015-09-15 12:35:22 -07001579 print api_id
maruel11e31af2017-02-15 07:30:50 -08001580 print ' ' + api['description'].strip()
1581 if 'resources' in api:
1582 # Old.
1583 for j, (resource_name, resource) in enumerate(
1584 sorted(api['resources'].iteritems())):
1585 if j:
1586 print('')
1587 for method_name, method in sorted(resource['methods'].iteritems()):
1588 # Only list the GET ones.
1589 if method['httpMethod'] != 'GET':
1590 continue
1591 print '- %s.%s: %s' % (
1592 resource_name, method_name, method['path'])
1593 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001594 ' ' + l for l in textwrap.wrap(
1595 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001596 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1597 else:
1598 # New.
1599 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001600 # Only list the GET ones.
1601 if method['httpMethod'] != 'GET':
1602 continue
maruel11e31af2017-02-15 07:30:50 -08001603 print '- %s: %s' % (method['id'], method['path'])
1604 print('\n'.join(
1605 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001606 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1607 return 0
1608
1609
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001610@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001611def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001612 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001613
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001614 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001615 """
1616 add_trigger_options(parser)
1617 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001618 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001619 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001620 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001621 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001622 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001623 tasks = trigger_task_shards(
1624 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001625 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001626 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001627 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001628 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001629 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001630 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001631 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001632 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001633 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001634 task_ids = [
1635 t['task_id']
1636 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1637 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001638 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001639 offset = 0
1640 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001641 m = (offset + s.properties.execution_timeout_secs +
1642 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001643 if m > options.timeout:
1644 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001645 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001646 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001647 try:
1648 return collect(
1649 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001650 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001651 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001652 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001653 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001654 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001655 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001656 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001657 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001658 except Failure:
1659 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001660 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001661
1662
maruel18122c62015-10-23 06:31:23 -07001663@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001664def CMDreproduce(parser, args):
1665 """Runs a task locally that was triggered on the server.
1666
1667 This running locally the same commands that have been run on the bot. The data
1668 downloaded will be in a subdirectory named 'work' of the current working
1669 directory.
maruel18122c62015-10-23 06:31:23 -07001670
1671 You can pass further additional arguments to the target command by passing
1672 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001673 """
maruelc070e672016-02-22 17:32:57 -08001674 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001675 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001676 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001677 parser.add_option(
1678 '--work', metavar='DIR', default='work',
1679 help='Directory to map the task input files into')
1680 parser.add_option(
1681 '--cache', metavar='DIR', default='cache',
1682 help='Directory that contains the input cache')
1683 parser.add_option(
1684 '--leak', action='store_true',
1685 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001686 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001687 extra_args = []
1688 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001689 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001690 if len(args) > 1:
1691 if args[1] == '--':
1692 if len(args) > 2:
1693 extra_args = args[2:]
1694 else:
1695 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001696
smut281c3902018-05-30 17:50:05 -07001697 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001698 request = net.url_read_json(url)
1699 if not request:
1700 print >> sys.stderr, 'Failed to retrieve request data for the task'
1701 return 1
1702
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001703 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001704 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001705 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001706 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001707 cachedir = unicode(os.path.abspath('cipd_cache'))
1708 if not fs.exists(cachedir):
1709 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001710
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001711 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001712 env = os.environ.copy()
1713 env['SWARMING_BOT_ID'] = 'reproduce'
1714 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001715 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001716 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001717 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001718 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001719 if not i['value']:
1720 env.pop(key, None)
1721 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001722 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001723
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001724 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001725 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001726 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001727 for i in env_prefixes:
1728 key = i['key']
1729 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001730 cur = env.get(key)
1731 if cur:
1732 paths.append(cur)
1733 env[key] = os.path.pathsep.join(paths)
1734
iannucci31ab9192017-05-02 19:11:56 -07001735 command = []
nodir152cba62016-05-12 16:08:56 -07001736 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001737 # Create the tree.
1738 with isolateserver.get_storage(
1739 properties['inputs_ref']['isolatedserver'],
1740 properties['inputs_ref']['namespace']) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001741 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1742 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1743 # leak.
1744 policies = local_caching.CachePolicies(0, 0, 0, 0)
1745 algo = isolated_format.get_hash_algo(
1746 properties['inputs_ref']['namespace'])
1747 cache = local_caching.DiskContentAddressedCache(
1748 unicode(os.path.abspath(options.cache)), policies, algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001749 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001750 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001751 command = bundle.command
1752 if bundle.relative_cwd:
1753 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001754 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001755
1756 if properties.get('command'):
1757 command.extend(properties['command'])
1758
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001759 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001760 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001761 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001762 new_command = run_isolated.process_command(command, 'invalid', None)
1763 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001764 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001765 else:
1766 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001767 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001768 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001769 command, options.output, None)
1770 if not os.path.isdir(options.output):
1771 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001772 command = new_command
1773 file_path.ensure_command_has_abs_path(command, workdir)
1774
1775 if properties.get('cipd_input'):
1776 ci = properties['cipd_input']
1777 cp = ci['client_package']
1778 client_manager = cipd.get_client(
1779 ci['server'], cp['package_name'], cp['version'], cachedir)
1780
1781 with client_manager as client:
1782 by_path = collections.defaultdict(list)
1783 for pkg in ci['packages']:
1784 path = pkg['path']
1785 # cipd deals with 'root' as ''
1786 if path == '.':
1787 path = ''
1788 by_path[path].append((pkg['package_name'], pkg['version']))
1789 client.ensure(workdir, by_path, cache_dir=cachedir)
1790
maruel77f720b2015-09-15 12:35:22 -07001791 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001792 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001793 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001794 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001795 print >> sys.stderr, str(e)
1796 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001797 finally:
1798 # Do not delete options.cache.
1799 if not options.leak:
1800 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001801
1802
maruel0eb1d1b2015-10-02 14:48:21 -07001803@subcommand.usage('bot_id')
1804def CMDterminate(parser, args):
1805 """Tells a bot to gracefully shut itself down as soon as it can.
1806
1807 This is done by completing whatever current task there is then exiting the bot
1808 process.
1809 """
1810 parser.add_option(
1811 '--wait', action='store_true', help='Wait for the bot to terminate')
1812 options, args = parser.parse_args(args)
1813 if len(args) != 1:
1814 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001815 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001816 request = net.url_read_json(url, data={})
1817 if not request:
1818 print >> sys.stderr, 'Failed to ask for termination'
1819 return 1
1820 if options.wait:
1821 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001822 options.swarming,
1823 [request['task_id']],
1824 0.,
1825 False,
1826 False,
1827 None,
1828 None,
1829 [],
maruel9531ce02016-04-13 06:11:23 -07001830 False)
maruelbfc5f872017-06-10 16:43:17 -07001831 else:
1832 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001833 return 0
1834
1835
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001836@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001837def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001838 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001839
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001840 Passes all extra arguments provided after '--' as additional command line
1841 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001842 """
1843 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001844 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001845 parser.add_option(
1846 '--dump-json',
1847 metavar='FILE',
1848 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001849 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001850 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001851 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001852 tasks = trigger_task_shards(
1853 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001854 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001855 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001856 tasks_sorted = sorted(
1857 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001858 if options.dump_json:
1859 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001860 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001861 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001862 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001863 }
maruel46b015f2015-10-13 18:40:35 -07001864 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001865 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001866 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001867 (options.swarming, options.dump_json))
1868 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001869 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001870 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001871 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1872 print('Or visit:')
1873 for t in tasks_sorted:
1874 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001875 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001876 except Failure:
1877 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001878 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001879
1880
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001881class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001882 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001883 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001884 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001885 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001886 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001887 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001888 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001889 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001890 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001891 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001892
1893 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001894 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001895 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001896 auth.process_auth_options(self, options)
1897 user = self._process_swarming(options)
1898 if hasattr(options, 'user') and not options.user:
1899 options.user = user
1900 return options, args
1901
1902 def _process_swarming(self, options):
1903 """Processes the --swarming option and aborts if not specified.
1904
1905 Returns the identity as determined by the server.
1906 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001907 if not options.swarming:
1908 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001909 try:
1910 options.swarming = net.fix_url(options.swarming)
1911 except ValueError as e:
1912 self.error('--swarming %s' % e)
1913 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001914 try:
1915 user = auth.ensure_logged_in(options.swarming)
1916 except ValueError as e:
1917 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001918 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001919
1920
1921def main(args):
1922 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001923 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001924
1925
1926if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001927 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001928 fix_encoding.fix_encoding()
1929 tools.disable_buffering()
1930 colorama.init()
1931 sys.exit(main(sys.argv[1:]))