blob: 32681e3ca318105c0c183f59f55469cab601112b [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')
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400177 out['task_slices'][0]['properties']['dimensions'] = [
maruel77f720b2015-09-15 12:35:22 -0700178 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400179 for k, v in out['task_slices'][0]['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700180 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400181 out['task_slices'][0]['properties']['env'] = [
maruel77f720b2015-09-15 12:35:22 -0700182 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400183 for k, v in out['task_slices'][0]['properties']['env'].iteritems()
maruel77f720b2015-09-15 12:35:22 -0700184 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400185 out['task_slices'][0]['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700186 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500187
188
maruel77f720b2015-09-15 12:35:22 -0700189def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500190 """Triggers a request on the Swarming server and returns the json data.
191
192 It's the low-level function.
193
194 Returns:
195 {
196 'request': {
197 'created_ts': u'2010-01-02 03:04:05',
198 'name': ..
199 },
200 'task_id': '12300',
201 }
202 """
203 logging.info('Triggering: %s', raw_request['name'])
204
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500205 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700206 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500207 if not result:
208 on_error.report('Failed to trigger task %s' % raw_request['name'])
209 return None
maruele557bce2015-11-17 09:01:27 -0800210 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800211 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800212 msg = 'Failed to trigger task %s' % raw_request['name']
213 if result['error'].get('errors'):
214 for err in result['error']['errors']:
215 if err.get('message'):
216 msg += '\nMessage: %s' % err['message']
217 if err.get('debugInfo'):
218 msg += '\nDebug info:\n%s' % err['debugInfo']
219 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800220 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800221
222 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800223 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500224 return result
225
226
227def setup_googletest(env, shards, index):
228 """Sets googletest specific environment variables."""
229 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700230 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
231 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
232 env = env[:]
233 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
234 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500235 return env
236
237
238def trigger_task_shards(swarming, task_request, shards):
239 """Triggers one or many subtasks of a sharded task.
240
241 Returns:
242 Dict with task details, returned to caller as part of --dump-json output.
243 None in case of failure.
244 """
245 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700246 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500247 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400248 req['task_slices'][0]['properties']['env'] = setup_googletest(
249 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700250 req['name'] += ':%s:%s' % (index, shards)
251 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500252
253 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500254 tasks = {}
255 priority_warning = False
256 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700257 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500258 if not task:
259 break
260 logging.info('Request result: %s', task)
261 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400262 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500263 priority_warning = True
264 print >> sys.stderr, (
265 'Priority was reset to %s' % task['request']['priority'])
266 tasks[request['name']] = {
267 'shard_index': index,
268 'task_id': task['task_id'],
269 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
270 }
271
272 # Some shards weren't triggered. Abort everything.
273 if len(tasks) != len(requests):
274 if tasks:
275 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
276 len(tasks), len(requests))
277 for task_dict in tasks.itervalues():
278 abort_task(swarming, task_dict['task_id'])
279 return None
280
281 return tasks
282
283
284### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000285
286
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700287# How often to print status updates to stdout in 'collect'.
288STATUS_UPDATE_INTERVAL = 15 * 60.
289
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400290
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000291class TaskState(object):
292 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000293
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000294 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
295 is the source of truth for these values:
296 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400297
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000298 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400299 """
300 RUNNING = 0x10
301 PENDING = 0x20
302 EXPIRED = 0x30
303 TIMED_OUT = 0x40
304 BOT_DIED = 0x50
305 CANCELED = 0x60
306 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400307 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400308 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400309
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000310 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400311
maruel77f720b2015-09-15 12:35:22 -0700312 _ENUMS = {
313 'RUNNING': RUNNING,
314 'PENDING': PENDING,
315 'EXPIRED': EXPIRED,
316 'TIMED_OUT': TIMED_OUT,
317 'BOT_DIED': BOT_DIED,
318 'CANCELED': CANCELED,
319 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400320 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400321 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700322 }
323
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400324 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700325 def from_enum(cls, state):
326 """Returns int value based on the string."""
327 if state not in cls._ENUMS:
328 raise ValueError('Invalid state %s' % state)
329 return cls._ENUMS[state]
330
maruel@chromium.org0437a732013-08-27 16:05:52 +0000331
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700332class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700333 """Assembles task execution summary (for --task-summary-json output).
334
335 Optionally fetches task outputs from isolate server to local disk (used when
336 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700337
338 This object is shared among multiple threads running 'retrieve_results'
339 function, in particular they call 'process_shard_result' method in parallel.
340 """
341
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000342 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700343 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
344
345 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700346 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700347 shard_count: expected number of task shards.
348 """
maruel12e30012015-10-09 11:55:35 -0700349 self.task_output_dir = (
350 unicode(os.path.abspath(task_output_dir))
351 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000352 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353 self.shard_count = shard_count
354
355 self._lock = threading.Lock()
356 self._per_shard_results = {}
357 self._storage = None
358
nodire5028a92016-04-29 14:38:21 -0700359 if self.task_output_dir:
360 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361
Vadim Shtayurab450c602014-05-12 19:23:25 -0700362 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 """Stores results of a single task shard, fetches output files if necessary.
364
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400365 Modifies |result| in place.
366
maruel77f720b2015-09-15 12:35:22 -0700367 shard_index is 0-based.
368
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369 Called concurrently from multiple threads.
370 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700371 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700372 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700373 if shard_index < 0 or shard_index >= self.shard_count:
374 logging.warning(
375 'Shard index %d is outside of expected range: [0; %d]',
376 shard_index, self.shard_count - 1)
377 return
378
maruel77f720b2015-09-15 12:35:22 -0700379 if result.get('outputs_ref'):
380 ref = result['outputs_ref']
381 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
382 ref['isolatedserver'],
383 urllib.urlencode(
384 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400385
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 # Store result dict of that shard, ignore results we've already seen.
387 with self._lock:
388 if shard_index in self._per_shard_results:
389 logging.warning('Ignoring duplicate shard index %d', shard_index)
390 return
391 self._per_shard_results[shard_index] = result
392
393 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700394 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400395 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700396 result['outputs_ref']['isolatedserver'],
397 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400398 if storage:
399 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400400 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
401 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400402 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700403 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400404 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400405 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700406 os.path.join(self.task_output_dir, str(shard_index)),
407 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700408
409 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700410 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700411 with self._lock:
412 # Write an array of shard results with None for missing shards.
413 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700414 'shards': [
415 self._per_shard_results.get(i) for i in xrange(self.shard_count)
416 ],
417 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000418
419 # Don't store stdout in the summary if not requested too.
420 if "json" not in self.task_output_stdout:
421 for shard_json in summary['shards']:
422 if not shard_json:
423 continue
424 if "output" in shard_json:
425 del shard_json["output"]
426 if "outputs" in shard_json:
427 del shard_json["outputs"]
428
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700429 # Write summary.json to task_output_dir as well.
430 if self.task_output_dir:
431 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700432 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700433 summary,
434 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700435 if self._storage:
436 self._storage.close()
437 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700438 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439
440 def _get_storage(self, isolate_server, namespace):
441 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 with self._lock:
444 if not self._storage:
445 self._storage = isolateserver.get_storage(isolate_server, namespace)
446 else:
447 # Shards must all use exact same isolate server and namespace.
448 if self._storage.location != isolate_server:
449 logging.error(
450 'Task shards are using multiple isolate servers: %s and %s',
451 self._storage.location, isolate_server)
452 return None
453 if self._storage.namespace != namespace:
454 logging.error(
455 'Task shards are using multiple namespaces: %s and %s',
456 self._storage.namespace, namespace)
457 return None
458 return self._storage
459
460
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500461def now():
462 """Exists so it can be mocked easily."""
463 return time.time()
464
465
maruel77f720b2015-09-15 12:35:22 -0700466def parse_time(value):
467 """Converts serialized time from the API to datetime.datetime."""
468 # When microseconds are 0, the '.123456' suffix is elided. This means the
469 # serialized format is not consistent, which confuses the hell out of python.
470 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
471 try:
472 return datetime.datetime.strptime(value, fmt)
473 except ValueError:
474 pass
475 raise ValueError('Failed to parse %s' % value)
476
477
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700478def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700479 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000480 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400481 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700482
Vadim Shtayurab450c602014-05-12 19:23:25 -0700483 Returns:
484 <result dict> on success.
485 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700486 """
maruel71c61c82016-02-22 06:52:05 -0800487 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700488 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700489 if include_perf:
490 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700491 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700492 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400493 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494 attempt = 0
495
496 while not should_stop.is_set():
497 attempt += 1
498
499 # Waiting for too long -> give up.
500 current_time = now()
501 if deadline and current_time >= deadline:
502 logging.error('retrieve_results(%s) timed out on attempt %d',
503 base_url, attempt)
504 return None
505
506 # Do not spin too fast. Spin faster at the beginning though.
507 # Start with 1 sec delay and for each 30 sec of waiting add another second
508 # of delay, until hitting 15 sec ceiling.
509 if attempt > 1:
510 max_delay = min(15, 1 + (current_time - started) / 30.0)
511 delay = min(max_delay, deadline - current_time) if deadline else max_delay
512 if delay > 0:
513 logging.debug('Waiting %.1f sec before retrying', delay)
514 should_stop.wait(delay)
515 if should_stop.is_set():
516 return None
517
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400518 # Disable internal retries in net.url_read_json, since we are doing retries
519 # ourselves.
520 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700521 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
522 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400523 # Retry on 500s only if no timeout is specified.
524 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400525 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400526 if timeout == -1:
527 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400528 continue
maruel77f720b2015-09-15 12:35:22 -0700529
maruelbf53e042015-12-01 15:00:51 -0800530 if result.get('error'):
531 # An error occurred.
532 if result['error'].get('errors'):
533 for err in result['error']['errors']:
534 logging.warning(
535 'Error while reading task: %s; %s',
536 err.get('message'), err.get('debugInfo'))
537 elif result['error'].get('message'):
538 logging.warning(
539 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400540 if timeout == -1:
541 return result
maruelbf53e042015-12-01 15:00:51 -0800542 continue
543
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400544 # When timeout == -1, always return on first attempt. 500s are already
545 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000546 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000547 if fetch_stdout:
548 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700549 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700550 # Record the result, try to fetch attached output files (if any).
551 if output_collector:
552 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700553 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700554 if result.get('internal_failure'):
555 logging.error('Internal error!')
556 elif result['state'] == 'BOT_DIED':
557 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700558 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000559
560
maruel77f720b2015-09-15 12:35:22 -0700561def convert_to_old_format(result):
562 """Converts the task result data from Endpoints API format to old API format
563 for compatibility.
564
565 This goes into the file generated as --task-summary-json.
566 """
567 # Sets default.
568 result.setdefault('abandoned_ts', None)
569 result.setdefault('bot_id', None)
570 result.setdefault('bot_version', None)
571 result.setdefault('children_task_ids', [])
572 result.setdefault('completed_ts', None)
573 result.setdefault('cost_saved_usd', None)
574 result.setdefault('costs_usd', None)
575 result.setdefault('deduped_from', None)
576 result.setdefault('name', None)
577 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700578 result.setdefault('server_versions', None)
579 result.setdefault('started_ts', None)
580 result.setdefault('tags', None)
581 result.setdefault('user', None)
582
583 # Convertion back to old API.
584 duration = result.pop('duration', None)
585 result['durations'] = [duration] if duration else []
586 exit_code = result.pop('exit_code', None)
587 result['exit_codes'] = [int(exit_code)] if exit_code else []
588 result['id'] = result.pop('task_id')
589 result['isolated_out'] = result.get('outputs_ref', None)
590 output = result.pop('output', None)
591 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700592 # server_version
593 # Endpoints result 'state' as string. For compatibility with old code, convert
594 # to int.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000595 result['state'] = TaskState.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700596 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700597 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700598 if 'bot_dimensions' in result:
599 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700600 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700601 }
602 else:
603 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700604
605
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700606def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400607 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000608 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500609 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000610
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700611 Duplicate shards are ignored. Shards are yielded in order of completion.
612 Timed out shards are NOT yielded at all. Caller can compare number of yielded
613 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000614
615 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500616 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 +0000617 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500618
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700619 output_collector is an optional instance of TaskOutputCollector that will be
620 used to fetch files produced by a task from isolate server to the local disk.
621
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500622 Yields:
623 (index, result). In particular, 'result' is defined as the
624 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000626 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400627 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700628 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700629 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700630
maruel@chromium.org0437a732013-08-27 16:05:52 +0000631 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
632 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 # Adds a task to the thread pool to call 'retrieve_results' and return
634 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400635 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700636 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000637 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400638 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000639 task_id, timeout, should_stop, output_collector, include_perf,
640 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700641
642 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400643 for shard_index, task_id in enumerate(task_ids):
644 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700645
646 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400647 shards_remaining = range(len(task_ids))
648 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700651 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652 shard_index, result = results_channel.pull(
653 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700654 except threading_utils.TaskChannel.Timeout:
655 if print_status_updates:
656 print(
657 'Waiting for results from the following shards: %s' %
658 ', '.join(map(str, shards_remaining)))
659 sys.stdout.flush()
660 continue
661 except Exception:
662 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700663
664 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700665 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000666 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500667 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000668 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700669
Vadim Shtayurab450c602014-05-12 19:23:25 -0700670 # Yield back results to the caller.
671 assert shard_index in shards_remaining
672 shards_remaining.remove(shard_index)
673 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700674
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700676 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677 should_stop.set()
678
679
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000680def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700682 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400683 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700684 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
685 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400686 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
687 metadata.get('abandoned_ts')):
688 pending = '%.1fs' % (
689 parse_time(metadata['abandoned_ts']) -
690 parse_time(metadata['created_ts'])
691 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400692 else:
693 pending = 'N/A'
694
maruel77f720b2015-09-15 12:35:22 -0700695 if metadata.get('duration') is not None:
696 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400697 else:
698 duration = 'N/A'
699
maruel77f720b2015-09-15 12:35:22 -0700700 if metadata.get('exit_code') is not None:
701 # Integers are encoded as string to not loose precision.
702 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400703 else:
704 exit_code = 'N/A'
705
706 bot_id = metadata.get('bot_id') or 'N/A'
707
maruel77f720b2015-09-15 12:35:22 -0700708 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400709 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000710 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400711 if metadata.get('state') == 'CANCELED':
712 tag_footer2 = ' Pending: %s CANCELED' % pending
713 elif metadata.get('state') == 'EXPIRED':
714 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400715 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400716 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
717 pending, duration, bot_id, exit_code, metadata['state'])
718 else:
719 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
720 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400721
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000722 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
723 dash_pad = '+-%s-+' % ('-' * tag_len)
724 tag_header = '| %s |' % tag_header.ljust(tag_len)
725 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
726 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400727
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000728 if include_stdout:
729 return '\n'.join([
730 dash_pad,
731 tag_header,
732 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400733 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000734 dash_pad,
735 tag_footer1,
736 tag_footer2,
737 dash_pad,
738 ])
739 else:
740 return '\n'.join([
741 dash_pad,
742 tag_header,
743 tag_footer2,
744 dash_pad,
745 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000746
747
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700748def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700749 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000750 task_summary_json, task_output_dir, task_output_stdout,
751 include_perf):
maruela5490782015-09-30 10:56:59 -0700752 """Retrieves results of a Swarming task.
753
754 Returns:
755 process exit code that should be returned to the user.
756 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700757 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000758 output_collector = TaskOutputCollector(
759 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700760
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700761 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700762 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400763 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700764 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400765 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400766 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000767 output_collector, include_perf,
768 (len(task_output_stdout) > 0),
769 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700770 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700771
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400772 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700773 shard_exit_code = metadata.get('exit_code')
774 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700775 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700776 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700777 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400778 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700779 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700780
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700781 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000782 s = decorate_shard_output(
783 swarming, index, metadata,
784 "console" in task_output_stdout).encode(
785 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700786 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400787 if len(seen_shards) < len(task_ids):
788 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700789 else:
maruel77f720b2015-09-15 12:35:22 -0700790 print('%s: %s %s' % (
791 metadata.get('bot_id', 'N/A'),
792 metadata['task_id'],
793 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000794 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700795 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400796 if output:
797 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700798 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700799 summary = output_collector.finalize()
800 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700801 # TODO(maruel): Make this optional.
802 for i in summary['shards']:
803 if i:
804 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700805 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700806
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400807 if decorate and total_duration:
808 print('Total duration: %.1fs' % total_duration)
809
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400810 if len(seen_shards) != len(task_ids):
811 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700812 print >> sys.stderr, ('Results from some shards are missing: %s' %
813 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700814 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700815
maruela5490782015-09-30 10:56:59 -0700816 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000817
818
maruel77f720b2015-09-15 12:35:22 -0700819### API management.
820
821
822class APIError(Exception):
823 pass
824
825
826def endpoints_api_discovery_apis(host):
827 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
828 the APIs exposed by a host.
829
830 https://developers.google.com/discovery/v1/reference/apis/list
831 """
maruel380e3262016-08-31 16:10:06 -0700832 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
833 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700834 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
835 if data is None:
836 raise APIError('Failed to discover APIs on %s' % host)
837 out = {}
838 for api in data['items']:
839 if api['id'] == 'discovery:v1':
840 continue
841 # URL is of the following form:
842 # url = host + (
843 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
844 api_data = net.url_read_json(api['discoveryRestUrl'])
845 if api_data is None:
846 raise APIError('Failed to discover %s on %s' % (api['id'], host))
847 out[api['id']] = api_data
848 return out
849
850
maruelaf6b06c2017-06-08 06:26:53 -0700851def get_yielder(base_url, limit):
852 """Returns the first query and a function that yields following items."""
853 CHUNK_SIZE = 250
854
855 url = base_url
856 if limit:
857 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
858 data = net.url_read_json(url)
859 if data is None:
860 # TODO(maruel): Do basic diagnostic.
861 raise Failure('Failed to access %s' % url)
862 org_cursor = data.pop('cursor', None)
863 org_total = len(data.get('items') or [])
864 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
865 if not org_cursor or not org_total:
866 # This is not an iterable resource.
867 return data, lambda: []
868
869 def yielder():
870 cursor = org_cursor
871 total = org_total
872 # Some items support cursors. Try to get automatically if cursors are needed
873 # by looking at the 'cursor' items.
874 while cursor and (not limit or total < limit):
875 merge_char = '&' if '?' in base_url else '?'
876 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
877 if limit:
878 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
879 new = net.url_read_json(url)
880 if new is None:
881 raise Failure('Failed to access %s' % url)
882 cursor = new.get('cursor')
883 new_items = new.get('items')
884 nb_items = len(new_items or [])
885 total += nb_items
886 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
887 yield new_items
888
889 return data, yielder
890
891
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500892### Commands.
893
894
895def abort_task(_swarming, _manifest):
896 """Given a task manifest that was triggered, aborts its execution."""
897 # TODO(vadimsh): No supported by the server yet.
898
899
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400900def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800901 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500902 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500903 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500904 dest='dimensions', metavar='FOO bar',
905 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500906 parser.add_option_group(parser.filter_group)
907
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400908
maruelaf6b06c2017-06-08 06:26:53 -0700909def process_filter_options(parser, options):
910 for key, value in options.dimensions:
911 if ':' in key:
912 parser.error('--dimension key cannot contain ":"')
913 if key.strip() != key:
914 parser.error('--dimension key has whitespace')
915 if not key:
916 parser.error('--dimension key is empty')
917
918 if value.strip() != value:
919 parser.error('--dimension value has whitespace')
920 if not value:
921 parser.error('--dimension value is empty')
922 options.dimensions.sort()
923
924
Vadim Shtayurab450c602014-05-12 19:23:25 -0700925def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400926 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700927 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700928 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700929 help='Number of shards to trigger and collect.')
930 parser.add_option_group(parser.sharding_group)
931
932
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400933def add_trigger_options(parser):
934 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500935 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400936 add_filter_options(parser)
937
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400938 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800939 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700940 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500941 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800942 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500943 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700944 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800945 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800946 '--env-prefix', default=[], action='append', nargs=2,
947 metavar='VAR local/path',
948 help='Prepend task-relative `local/path` to the task\'s VAR environment '
949 'variable using os-appropriate pathsep character. Can be specified '
950 'multiple times for the same VAR to add multiple paths.')
951 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400952 '--idempotent', action='store_true', default=False,
953 help='When set, the server will actively try to find a previous task '
954 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800955 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700956 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700957 help='The optional path to a file containing the secret_bytes to use with'
958 'this task.')
maruel681d6802017-01-17 16:56:03 -0800959 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700960 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400961 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800962 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700963 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400964 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800965 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500966 '--raw-cmd', action='store_true', default=False,
967 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700968 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800969 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500970 '--relative-cwd',
971 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
972 'requires --raw-cmd')
973 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700974 '--cipd-package', action='append', default=[], metavar='PKG',
975 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700976 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800977 group.add_option(
978 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700979 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800980 help='"<name> <relpath>" items to keep a persistent bot managed cache')
981 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700982 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700983 help='Email of a service account to run the task as, or literal "bot" '
984 'string to indicate that the task should use the same account the '
985 'bot itself is using to authenticate to Swarming. Don\'t use task '
986 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800987 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +0000988 '--pool-task-template',
989 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
990 default='AUTO',
991 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
992 'By default, the pool\'s TaskTemplate is automatically selected, '
993 'according the pool configuration on the server. Choices are: '
994 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
995 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700996 '-o', '--output', action='append', default=[], metavar='PATH',
997 help='A list of files to return in addition to those written to '
998 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
999 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001000 group.add_option(
1001 '--wait-for-capacity', action='store_true', default=False,
1002 help='Instructs to leave the task PENDING even if there\'s no known bot '
1003 'that could run this task, otherwise the task will be denied with '
1004 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001005 parser.add_option_group(group)
1006
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001007 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001008 group.add_option(
1009 '--priority', type='int', default=100,
1010 help='The lower value, the more important the task is')
1011 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001012 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001013 help='Display name of the task. Defaults to '
1014 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1015 'isolated file is provided, if a hash is provided, it defaults to '
1016 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1017 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001018 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001019 help='Tags to assign to the task.')
1020 group.add_option(
1021 '--user', default='',
1022 help='User associated with the task. Defaults to authenticated user on '
1023 'the server.')
1024 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001025 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001026 help='Seconds to allow the task to be pending for a bot to run before '
1027 'this task request expires.')
1028 group.add_option(
1029 '--deadline', type='int', dest='expiration',
1030 help=optparse.SUPPRESS_HELP)
1031 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001032
1033
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001034def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001035 """Processes trigger options and does preparatory steps.
1036
1037 Returns:
1038 NewTaskRequest instance.
1039 """
maruelaf6b06c2017-06-08 06:26:53 -07001040 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001041 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001042 if args and args[0] == '--':
1043 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001044
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001045 if not options.dimensions:
1046 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001047 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1048 parser.error('--tags must be in the format key:value')
1049 if options.raw_cmd and not args:
1050 parser.error(
1051 'Arguments with --raw-cmd should be passed after -- as command '
1052 'delimiter.')
1053 if options.isolate_server and not options.namespace:
1054 parser.error(
1055 '--namespace must be a valid value when --isolate-server is used')
1056 if not options.isolated and not options.raw_cmd:
1057 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1058
1059 # Isolated
1060 # --isolated is required only if --raw-cmd wasn't provided.
1061 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1062 # preferred server.
1063 isolateserver.process_isolate_server_options(
1064 parser, options, False, not options.raw_cmd)
1065 inputs_ref = None
1066 if options.isolate_server:
1067 inputs_ref = FilesRef(
1068 isolated=options.isolated,
1069 isolatedserver=options.isolate_server,
1070 namespace=options.namespace)
1071
1072 # Command
1073 command = None
1074 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001075 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001077 if options.relative_cwd:
1078 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1079 if not a.startswith(os.getcwd()):
1080 parser.error(
1081 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001082 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001083 if options.relative_cwd:
1084 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001085 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001086
maruel0a25f6c2017-05-10 10:43:23 -07001087 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001088 cipd_packages = []
1089 for p in options.cipd_package:
1090 split = p.split(':', 2)
1091 if len(split) != 3:
1092 parser.error('CIPD packages must take the form: path:package:version')
1093 cipd_packages.append(CipdPackage(
1094 package_name=split[1],
1095 path=split[0],
1096 version=split[2]))
1097 cipd_input = None
1098 if cipd_packages:
1099 cipd_input = CipdInput(
1100 client_package=None,
1101 packages=cipd_packages,
1102 server=None)
1103
maruel0a25f6c2017-05-10 10:43:23 -07001104 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001105 secret_bytes = None
1106 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001107 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001108 secret_bytes = f.read().encode('base64')
1109
maruel0a25f6c2017-05-10 10:43:23 -07001110 # Named caches
maruel681d6802017-01-17 16:56:03 -08001111 caches = [
1112 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1113 for i in options.named_cache
1114 ]
maruel0a25f6c2017-05-10 10:43:23 -07001115
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001116 env_prefixes = {}
1117 for k, v in options.env_prefix:
1118 env_prefixes.setdefault(k, []).append(v)
1119
maruel77f720b2015-09-15 12:35:22 -07001120 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001121 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001122 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001123 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001124 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001125 dimensions=options.dimensions,
1126 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001127 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001128 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001129 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001130 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001131 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001132 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001133 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001134 outputs=options.output,
1135 secret_bytes=secret_bytes)
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001136 task_slice = TaskSlice(
1137 expiration_secs=options.expiration,
1138 properties=properties,
1139 wait_for_capacity=options.wait_for_capacity)
maruel77f720b2015-09-15 12:35:22 -07001140 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001141 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001142 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001143 priority=options.priority,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001144 task_slices=[task_slice],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001145 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001146 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001147 user=options.user,
1148 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001149
1150
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001151class TaskOutputStdoutOption(optparse.Option):
1152 """Where to output the each task's console output (stderr/stdout).
1153
1154 The output will be;
1155 none - not be downloaded.
1156 json - stored in summary.json file *only*.
1157 console - shown on stdout *only*.
1158 all - stored in summary.json and shown on stdout.
1159 """
1160
1161 choices = ['all', 'json', 'console', 'none']
1162
1163 def __init__(self, *args, **kw):
1164 optparse.Option.__init__(
1165 self,
1166 *args,
1167 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001168 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001169 help=re.sub('\s\s*', ' ', self.__doc__),
1170 **kw)
1171
1172 def convert_value(self, opt, value):
1173 if value not in self.choices:
1174 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1175 self.get_opt_string(), self.choices, value))
1176 stdout_to = []
1177 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001178 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001179 elif value != 'none':
1180 stdout_to = [value]
1181 return stdout_to
1182
1183
maruel@chromium.org0437a732013-08-27 16:05:52 +00001184def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001185 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001186 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001187 help='Timeout to wait for result, set to -1 for no timeout and get '
1188 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001189 parser.group_logging.add_option(
1190 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001191 parser.group_logging.add_option(
1192 '--print-status-updates', action='store_true',
1193 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001194 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001195 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001196 '--task-summary-json',
1197 metavar='FILE',
1198 help='Dump a summary of task results to this file as json. It contains '
1199 'only shards statuses as know to server directly. Any output files '
1200 'emitted by the task can be collected by using --task-output-dir')
1201 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001202 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001203 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001204 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001205 'directory contains per-shard directory with output files produced '
1206 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001207 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001208 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001209 parser.task_output_group.add_option(
1210 '--perf', action='store_true', default=False,
1211 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001212 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001213
1214
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001215def process_collect_options(parser, options):
1216 # Only negative -1 is allowed, disallow other negative values.
1217 if options.timeout != -1 and options.timeout < 0:
1218 parser.error('Invalid --timeout value')
1219
1220
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001221@subcommand.usage('bots...')
1222def CMDbot_delete(parser, args):
1223 """Forcibly deletes bots from the Swarming server."""
1224 parser.add_option(
1225 '-f', '--force', action='store_true',
1226 help='Do not prompt for confirmation')
1227 options, args = parser.parse_args(args)
1228 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001229 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001230
1231 bots = sorted(args)
1232 if not options.force:
1233 print('Delete the following bots?')
1234 for bot in bots:
1235 print(' %s' % bot)
1236 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1237 print('Goodbye.')
1238 return 1
1239
1240 result = 0
1241 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001242 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001243 if net.url_read_json(url, data={}, method='POST') is None:
1244 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001245 result = 1
1246 return result
1247
1248
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001249def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001250 """Returns information about the bots connected to the Swarming server."""
1251 add_filter_options(parser)
1252 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001253 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001254 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001255 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001256 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001257 help='Keep both dead and alive bots')
1258 parser.filter_group.add_option(
1259 '--busy', action='store_true', help='Keep only busy bots')
1260 parser.filter_group.add_option(
1261 '--idle', action='store_true', help='Keep only idle bots')
1262 parser.filter_group.add_option(
1263 '--mp', action='store_true',
1264 help='Keep only Machine Provider managed bots')
1265 parser.filter_group.add_option(
1266 '--non-mp', action='store_true',
1267 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001268 parser.filter_group.add_option(
1269 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001270 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001271 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001272 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001273
1274 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001275 parser.error('Use only one of --keep-dead or --dead-only')
1276 if options.busy and options.idle:
1277 parser.error('Use only one of --busy or --idle')
1278 if options.mp and options.non_mp:
1279 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001280
smut281c3902018-05-30 17:50:05 -07001281 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001282 values = []
1283 if options.dead_only:
1284 values.append(('is_dead', 'TRUE'))
1285 elif options.keep_dead:
1286 values.append(('is_dead', 'NONE'))
1287 else:
1288 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001289
maruelaf6b06c2017-06-08 06:26:53 -07001290 if options.busy:
1291 values.append(('is_busy', 'TRUE'))
1292 elif options.idle:
1293 values.append(('is_busy', 'FALSE'))
1294 else:
1295 values.append(('is_busy', 'NONE'))
1296
1297 if options.mp:
1298 values.append(('is_mp', 'TRUE'))
1299 elif options.non_mp:
1300 values.append(('is_mp', 'FALSE'))
1301 else:
1302 values.append(('is_mp', 'NONE'))
1303
1304 for key, value in options.dimensions:
1305 values.append(('dimensions', '%s:%s' % (key, value)))
1306 url += urllib.urlencode(values)
1307 try:
1308 data, yielder = get_yielder(url, 0)
1309 bots = data.get('items') or []
1310 for items in yielder():
1311 if items:
1312 bots.extend(items)
1313 except Failure as e:
1314 sys.stderr.write('\n%s\n' % e)
1315 return 1
maruel77f720b2015-09-15 12:35:22 -07001316 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001317 print bot['bot_id']
1318 if not options.bare:
1319 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1320 print ' %s' % json.dumps(dimensions, sort_keys=True)
1321 if bot.get('task_id'):
1322 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001323 return 0
1324
1325
maruelfd0a90c2016-06-10 11:51:10 -07001326@subcommand.usage('task_id')
1327def CMDcancel(parser, args):
1328 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001329 parser.add_option(
1330 '-k', '--kill-running', action='store_true', default=False,
1331 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001332 options, args = parser.parse_args(args)
1333 if not args:
1334 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001335 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001336 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001337 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001338 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001339 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001340 print('Deleting %s failed. Probably already gone' % task_id)
1341 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001342 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001343 return 0
1344
1345
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001346@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001347def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001348 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001349
1350 The result can be in multiple part if the execution was sharded. It can
1351 potentially have retries.
1352 """
1353 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001354 parser.add_option(
1355 '-j', '--json',
1356 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001357 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001358 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001359 if not args and not options.json:
1360 parser.error('Must specify at least one task id or --json.')
1361 if args and options.json:
1362 parser.error('Only use one of task id or --json.')
1363
1364 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001365 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001366 try:
maruel1ceb3872015-10-14 06:10:44 -07001367 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001368 data = json.load(f)
1369 except (IOError, ValueError):
1370 parser.error('Failed to open %s' % options.json)
1371 try:
1372 tasks = sorted(
1373 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1374 args = [t['task_id'] for t in tasks]
1375 except (KeyError, TypeError):
1376 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001377 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001378 # Take in account all the task slices.
1379 offset = 0
1380 for s in data['request']['task_slices']:
1381 m = (offset + s['properties']['execution_timeout_secs'] +
1382 s['expiration_secs'])
1383 if m > options.timeout:
1384 options.timeout = m
1385 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001386 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001387 else:
1388 valid = frozenset('0123456789abcdef')
1389 if any(not valid.issuperset(task_id) for task_id in args):
1390 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001391
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001392 try:
1393 return collect(
1394 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001395 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001396 options.timeout,
1397 options.decorate,
1398 options.print_status_updates,
1399 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001400 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001401 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001402 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001403 except Failure:
1404 on_error.report(None)
1405 return 1
1406
1407
maruel77f720b2015-09-15 12:35:22 -07001408@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001409def CMDpost(parser, args):
1410 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1411
1412 Input data must be sent to stdin, result is printed to stdout.
1413
1414 If HTTP response code >= 400, returns non-zero.
1415 """
1416 options, args = parser.parse_args(args)
1417 if len(args) != 1:
1418 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001419 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001420 data = sys.stdin.read()
1421 try:
1422 resp = net.url_read(url, data=data, method='POST')
1423 except net.TimeoutError:
1424 sys.stderr.write('Timeout!\n')
1425 return 1
1426 if not resp:
1427 sys.stderr.write('No response!\n')
1428 return 1
1429 sys.stdout.write(resp)
1430 return 0
1431
1432
1433@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001434def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001435 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1436 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001437
1438 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001439 Raw task request and results:
1440 swarming.py query -S server-url.com task/123456/request
1441 swarming.py query -S server-url.com task/123456/result
1442
maruel77f720b2015-09-15 12:35:22 -07001443 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001444 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001445
maruelaf6b06c2017-06-08 06:26:53 -07001446 Listing last 10 tasks on a specific bot named 'bot1':
1447 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001448
maruelaf6b06c2017-06-08 06:26:53 -07001449 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001450 quoting is important!:
1451 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001452 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001453 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001454 parser.add_option(
1455 '-L', '--limit', type='int', default=200,
1456 help='Limit to enforce on limitless items (like number of tasks); '
1457 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001458 parser.add_option(
1459 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001460 parser.add_option(
1461 '--progress', action='store_true',
1462 help='Prints a dot at each request to show progress')
1463 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001464 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001465 parser.error(
1466 'Must specify only method name and optionally query args properly '
1467 'escaped.')
smut281c3902018-05-30 17:50:05 -07001468 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001469 try:
1470 data, yielder = get_yielder(base_url, options.limit)
1471 for items in yielder():
1472 if items:
1473 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001474 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001475 sys.stderr.write('.')
1476 sys.stderr.flush()
1477 except Failure as e:
1478 sys.stderr.write('\n%s\n' % e)
1479 return 1
maruel77f720b2015-09-15 12:35:22 -07001480 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001481 sys.stderr.write('\n')
1482 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001483 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001484 options.json = unicode(os.path.abspath(options.json))
1485 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001486 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001487 try:
maruel77f720b2015-09-15 12:35:22 -07001488 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001489 sys.stdout.write('\n')
1490 except IOError:
1491 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001492 return 0
1493
1494
maruel77f720b2015-09-15 12:35:22 -07001495def CMDquery_list(parser, args):
1496 """Returns list of all the Swarming APIs that can be used with command
1497 'query'.
1498 """
1499 parser.add_option(
1500 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1501 options, args = parser.parse_args(args)
1502 if args:
1503 parser.error('No argument allowed.')
1504
1505 try:
1506 apis = endpoints_api_discovery_apis(options.swarming)
1507 except APIError as e:
1508 parser.error(str(e))
1509 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001510 options.json = unicode(os.path.abspath(options.json))
1511 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001512 json.dump(apis, f)
1513 else:
1514 help_url = (
1515 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1516 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001517 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1518 if i:
1519 print('')
maruel77f720b2015-09-15 12:35:22 -07001520 print api_id
maruel11e31af2017-02-15 07:30:50 -08001521 print ' ' + api['description'].strip()
1522 if 'resources' in api:
1523 # Old.
1524 for j, (resource_name, resource) in enumerate(
1525 sorted(api['resources'].iteritems())):
1526 if j:
1527 print('')
1528 for method_name, method in sorted(resource['methods'].iteritems()):
1529 # Only list the GET ones.
1530 if method['httpMethod'] != 'GET':
1531 continue
1532 print '- %s.%s: %s' % (
1533 resource_name, method_name, method['path'])
1534 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001535 ' ' + l for l in textwrap.wrap(
1536 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001537 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1538 else:
1539 # New.
1540 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001541 # Only list the GET ones.
1542 if method['httpMethod'] != 'GET':
1543 continue
maruel11e31af2017-02-15 07:30:50 -08001544 print '- %s: %s' % (method['id'], method['path'])
1545 print('\n'.join(
1546 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001547 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1548 return 0
1549
1550
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001551@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001552def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001553 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001554
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001555 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001556 """
1557 add_trigger_options(parser)
1558 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001559 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001560 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001561 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001562 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001563 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001564 tasks = trigger_task_shards(
1565 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001566 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001567 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001568 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001569 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001570 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001571 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001572 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001573 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001574 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001575 task_ids = [
1576 t['task_id']
1577 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1578 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001579 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001580 offset = 0
1581 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001582 m = (offset + s.properties.execution_timeout_secs +
1583 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001584 if m > options.timeout:
1585 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001586 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001587 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001588 try:
1589 return collect(
1590 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001591 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001592 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001593 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001594 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001595 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001596 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001597 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001598 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001599 except Failure:
1600 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001601 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001602
1603
maruel18122c62015-10-23 06:31:23 -07001604@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001605def CMDreproduce(parser, args):
1606 """Runs a task locally that was triggered on the server.
1607
1608 This running locally the same commands that have been run on the bot. The data
1609 downloaded will be in a subdirectory named 'work' of the current working
1610 directory.
maruel18122c62015-10-23 06:31:23 -07001611
1612 You can pass further additional arguments to the target command by passing
1613 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001614 """
maruelc070e672016-02-22 17:32:57 -08001615 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001616 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001617 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001618 parser.add_option(
1619 '--work', metavar='DIR', default='work',
1620 help='Directory to map the task input files into')
1621 parser.add_option(
1622 '--cache', metavar='DIR', default='cache',
1623 help='Directory that contains the input cache')
1624 parser.add_option(
1625 '--leak', action='store_true',
1626 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001627 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001628 extra_args = []
1629 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001630 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001631 if len(args) > 1:
1632 if args[1] == '--':
1633 if len(args) > 2:
1634 extra_args = args[2:]
1635 else:
1636 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001637
smut281c3902018-05-30 17:50:05 -07001638 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001639 request = net.url_read_json(url)
1640 if not request:
1641 print >> sys.stderr, 'Failed to retrieve request data for the task'
1642 return 1
1643
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001644 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001645 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001646 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001647 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001648 cachedir = unicode(os.path.abspath('cipd_cache'))
1649 if not fs.exists(cachedir):
1650 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001651
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001652 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001653 env = os.environ.copy()
1654 env['SWARMING_BOT_ID'] = 'reproduce'
1655 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001656 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001657 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001658 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001659 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001660 if not i['value']:
1661 env.pop(key, None)
1662 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001663 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001664
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001665 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001666 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001667 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001668 for i in env_prefixes:
1669 key = i['key']
1670 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001671 cur = env.get(key)
1672 if cur:
1673 paths.append(cur)
1674 env[key] = os.path.pathsep.join(paths)
1675
iannucci31ab9192017-05-02 19:11:56 -07001676 command = []
nodir152cba62016-05-12 16:08:56 -07001677 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001678 # Create the tree.
1679 with isolateserver.get_storage(
1680 properties['inputs_ref']['isolatedserver'],
1681 properties['inputs_ref']['namespace']) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001682 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1683 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1684 # leak.
1685 policies = local_caching.CachePolicies(0, 0, 0, 0)
1686 algo = isolated_format.get_hash_algo(
1687 properties['inputs_ref']['namespace'])
1688 cache = local_caching.DiskContentAddressedCache(
1689 unicode(os.path.abspath(options.cache)), policies, algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001690 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001691 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001692 command = bundle.command
1693 if bundle.relative_cwd:
1694 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001695 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001696
1697 if properties.get('command'):
1698 command.extend(properties['command'])
1699
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001700 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001701 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001702 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001703 new_command = run_isolated.process_command(command, 'invalid', None)
1704 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001705 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001706 else:
1707 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001708 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001709 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001710 command, options.output, None)
1711 if not os.path.isdir(options.output):
1712 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001713 command = new_command
1714 file_path.ensure_command_has_abs_path(command, workdir)
1715
1716 if properties.get('cipd_input'):
1717 ci = properties['cipd_input']
1718 cp = ci['client_package']
1719 client_manager = cipd.get_client(
1720 ci['server'], cp['package_name'], cp['version'], cachedir)
1721
1722 with client_manager as client:
1723 by_path = collections.defaultdict(list)
1724 for pkg in ci['packages']:
1725 path = pkg['path']
1726 # cipd deals with 'root' as ''
1727 if path == '.':
1728 path = ''
1729 by_path[path].append((pkg['package_name'], pkg['version']))
1730 client.ensure(workdir, by_path, cache_dir=cachedir)
1731
maruel77f720b2015-09-15 12:35:22 -07001732 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001733 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001734 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001735 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001736 print >> sys.stderr, str(e)
1737 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001738 finally:
1739 # Do not delete options.cache.
1740 if not options.leak:
1741 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001742
1743
maruel0eb1d1b2015-10-02 14:48:21 -07001744@subcommand.usage('bot_id')
1745def CMDterminate(parser, args):
1746 """Tells a bot to gracefully shut itself down as soon as it can.
1747
1748 This is done by completing whatever current task there is then exiting the bot
1749 process.
1750 """
1751 parser.add_option(
1752 '--wait', action='store_true', help='Wait for the bot to terminate')
1753 options, args = parser.parse_args(args)
1754 if len(args) != 1:
1755 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001756 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001757 request = net.url_read_json(url, data={})
1758 if not request:
1759 print >> sys.stderr, 'Failed to ask for termination'
1760 return 1
1761 if options.wait:
1762 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001763 options.swarming,
1764 [request['task_id']],
1765 0.,
1766 False,
1767 False,
1768 None,
1769 None,
1770 [],
maruel9531ce02016-04-13 06:11:23 -07001771 False)
maruelbfc5f872017-06-10 16:43:17 -07001772 else:
1773 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001774 return 0
1775
1776
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001777@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001778def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001779 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001780
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001781 Passes all extra arguments provided after '--' as additional command line
1782 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001783 """
1784 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001785 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001786 parser.add_option(
1787 '--dump-json',
1788 metavar='FILE',
1789 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001790 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001791 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001792 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001793 tasks = trigger_task_shards(
1794 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001795 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001796 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001797 tasks_sorted = sorted(
1798 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001799 if options.dump_json:
1800 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001801 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001802 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001803 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001804 }
maruel46b015f2015-10-13 18:40:35 -07001805 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001806 print('To collect results, use:')
1807 print(' swarming.py collect -S %s --json %s' %
1808 (options.swarming, options.dump_json))
1809 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001810 print('To collect results, use:')
1811 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001812 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1813 print('Or visit:')
1814 for t in tasks_sorted:
1815 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001816 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001817 except Failure:
1818 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001819 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001820
1821
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001822class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001823 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001824 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001825 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001826 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001827 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001828 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001829 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001830 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001831 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001832 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001833
1834 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001835 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001836 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001837 auth.process_auth_options(self, options)
1838 user = self._process_swarming(options)
1839 if hasattr(options, 'user') and not options.user:
1840 options.user = user
1841 return options, args
1842
1843 def _process_swarming(self, options):
1844 """Processes the --swarming option and aborts if not specified.
1845
1846 Returns the identity as determined by the server.
1847 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001848 if not options.swarming:
1849 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001850 try:
1851 options.swarming = net.fix_url(options.swarming)
1852 except ValueError as e:
1853 self.error('--swarming %s' % e)
1854 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001855 try:
1856 user = auth.ensure_logged_in(options.swarming)
1857 except ValueError as e:
1858 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001859 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001860
1861
1862def main(args):
1863 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001864 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001865
1866
1867if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001868 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001869 fix_encoding.fix_encoding()
1870 tools.disable_buffering()
1871 colorama.init()
1872 sys.exit(main(sys.argv[1:]))