blob: c117e57ee83d87e50486dfbec4fa5ffe94998641 [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
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -04008__version__ = '0.12'
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 Ruel2666d9c2018-05-18 13:52:02 -040040import local_caching
maruelc070e672016-02-22 17:32:57 -080041import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000042
43
tansella4949442016-06-23 22:34:32 -070044ROOT_DIR = os.path.dirname(os.path.abspath(
45 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050046
47
48class Failure(Exception):
49 """Generic failure."""
50 pass
51
52
maruel0a25f6c2017-05-10 10:43:23 -070053def default_task_name(options):
54 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050055 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070056 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070057 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070058 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070059 if options.isolated:
60 task_name += u'/' + options.isolated
61 return task_name
62 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050063
64
65### Triggering.
66
67
maruel77f720b2015-09-15 12:35:22 -070068# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070069CipdPackage = collections.namedtuple(
70 'CipdPackage',
71 [
72 'package_name',
73 'path',
74 'version',
75 ])
76
77
78# See ../appengine/swarming/swarming_rpcs.py.
79CipdInput = collections.namedtuple(
80 'CipdInput',
81 [
82 'client_package',
83 'packages',
84 'server',
85 ])
86
87
88# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070089FilesRef = collections.namedtuple(
90 'FilesRef',
91 [
92 'isolated',
93 'isolatedserver',
94 'namespace',
95 ])
96
97
98# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080099StringListPair = collections.namedtuple(
100 'StringListPair', [
101 'key',
102 'value', # repeated string
103 ]
104)
105
106
107# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700108TaskProperties = collections.namedtuple(
109 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110 [
maruel681d6802017-01-17 16:56:03 -0800111 'caches',
borenet02f772b2016-06-22 12:42:19 -0700112 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500113 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500114 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500115 'dimensions',
116 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800117 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700118 'execution_timeout_secs',
119 'extra_args',
120 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500121 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700122 'inputs_ref',
123 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700124 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700125 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700126 ])
127
128
129# See ../appengine/swarming/swarming_rpcs.py.
130NewTaskRequest = collections.namedtuple(
131 'NewTaskRequest',
132 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500133 'name',
maruel77f720b2015-09-15 12:35:22 -0700134 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500135 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400136 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700137 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500138 'tags',
139 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500140 ])
141
142
maruel77f720b2015-09-15 12:35:22 -0700143def namedtuple_to_dict(value):
144 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400145 if hasattr(value, '_asdict'):
146 return namedtuple_to_dict(value._asdict())
147 if isinstance(value, (list, tuple)):
148 return [namedtuple_to_dict(v) for v in value]
149 if isinstance(value, dict):
150 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
151 return value
maruel77f720b2015-09-15 12:35:22 -0700152
153
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700154def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800155 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700156
157 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500158 """
maruel77f720b2015-09-15 12:35:22 -0700159 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700160 # Don't send 'service_account' if it is None to avoid confusing older
161 # version of the server that doesn't know about 'service_account' and don't
162 # use it at all.
163 if not out['service_account']:
164 out.pop('service_account')
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400165 out['task_slices'][0]['properties']['dimensions'] = [
maruel77f720b2015-09-15 12:35:22 -0700166 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400167 for k, v in out['task_slices'][0]['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700168 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400169 out['task_slices'][0]['properties']['env'] = [
maruel77f720b2015-09-15 12:35:22 -0700170 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400171 for k, v in out['task_slices'][0]['properties']['env'].iteritems()
maruel77f720b2015-09-15 12:35:22 -0700172 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400173 out['task_slices'][0]['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700174 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500175
176
maruel77f720b2015-09-15 12:35:22 -0700177def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500178 """Triggers a request on the Swarming server and returns the json data.
179
180 It's the low-level function.
181
182 Returns:
183 {
184 'request': {
185 'created_ts': u'2010-01-02 03:04:05',
186 'name': ..
187 },
188 'task_id': '12300',
189 }
190 """
191 logging.info('Triggering: %s', raw_request['name'])
192
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500193 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700194 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500195 if not result:
196 on_error.report('Failed to trigger task %s' % raw_request['name'])
197 return None
maruele557bce2015-11-17 09:01:27 -0800198 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800199 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800200 msg = 'Failed to trigger task %s' % raw_request['name']
201 if result['error'].get('errors'):
202 for err in result['error']['errors']:
203 if err.get('message'):
204 msg += '\nMessage: %s' % err['message']
205 if err.get('debugInfo'):
206 msg += '\nDebug info:\n%s' % err['debugInfo']
207 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800208 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800209
210 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800211 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500212 return result
213
214
215def setup_googletest(env, shards, index):
216 """Sets googletest specific environment variables."""
217 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700218 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
219 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
220 env = env[:]
221 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
222 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500223 return env
224
225
226def trigger_task_shards(swarming, task_request, shards):
227 """Triggers one or many subtasks of a sharded task.
228
229 Returns:
230 Dict with task details, returned to caller as part of --dump-json output.
231 None in case of failure.
232 """
233 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700234 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500235 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400236 req['task_slices'][0]['properties']['env'] = setup_googletest(
237 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700238 req['name'] += ':%s:%s' % (index, shards)
239 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500240
241 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242 tasks = {}
243 priority_warning = False
244 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700245 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500246 if not task:
247 break
248 logging.info('Request result: %s', task)
249 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400250 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251 priority_warning = True
252 print >> sys.stderr, (
253 'Priority was reset to %s' % task['request']['priority'])
254 tasks[request['name']] = {
255 'shard_index': index,
256 'task_id': task['task_id'],
257 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
258 }
259
260 # Some shards weren't triggered. Abort everything.
261 if len(tasks) != len(requests):
262 if tasks:
263 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
264 len(tasks), len(requests))
265 for task_dict in tasks.itervalues():
266 abort_task(swarming, task_dict['task_id'])
267 return None
268
269 return tasks
270
271
272### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000273
274
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700275# How often to print status updates to stdout in 'collect'.
276STATUS_UPDATE_INTERVAL = 15 * 60.
277
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400278
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400279class State(object):
280 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000281
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400282 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
283 values are part of the API so if they change, the API changed.
284
285 It's in fact an enum. Values should be in decreasing order of importance.
286 """
287 RUNNING = 0x10
288 PENDING = 0x20
289 EXPIRED = 0x30
290 TIMED_OUT = 0x40
291 BOT_DIED = 0x50
292 CANCELED = 0x60
293 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400294 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400295 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400296
maruel77f720b2015-09-15 12:35:22 -0700297 STATES = (
298 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400299 'COMPLETED', 'KILLED', 'NO_RESOURCE')
maruel77f720b2015-09-15 12:35:22 -0700300 STATES_RUNNING = ('RUNNING', 'PENDING')
301 STATES_NOT_RUNNING = (
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400302 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED', 'KILLED',
303 'NO_RESOURCE')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400304 STATES_DONE = ('TIMED_OUT', 'COMPLETED', 'KILLED')
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400305 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED', 'NO_RESOURCE')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400306
307 _NAMES = {
308 RUNNING: 'Running',
309 PENDING: 'Pending',
310 EXPIRED: 'Expired',
311 TIMED_OUT: 'Execution timed out',
312 BOT_DIED: 'Bot died',
313 CANCELED: 'User canceled',
314 COMPLETED: 'Completed',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400315 KILLED: 'User killed',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400316 NO_RESOURCE: 'No resource',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400317 }
318
maruel77f720b2015-09-15 12:35:22 -0700319 _ENUMS = {
320 'RUNNING': RUNNING,
321 'PENDING': PENDING,
322 'EXPIRED': EXPIRED,
323 'TIMED_OUT': TIMED_OUT,
324 'BOT_DIED': BOT_DIED,
325 'CANCELED': CANCELED,
326 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400327 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400328 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700329 }
330
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400331 @classmethod
332 def to_string(cls, state):
333 """Returns a user-readable string representing a State."""
334 if state not in cls._NAMES:
335 raise ValueError('Invalid state %s' % state)
336 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000337
maruel77f720b2015-09-15 12:35:22 -0700338 @classmethod
339 def from_enum(cls, state):
340 """Returns int value based on the string."""
341 if state not in cls._ENUMS:
342 raise ValueError('Invalid state %s' % state)
343 return cls._ENUMS[state]
344
maruel@chromium.org0437a732013-08-27 16:05:52 +0000345
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700346class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700347 """Assembles task execution summary (for --task-summary-json output).
348
349 Optionally fetches task outputs from isolate server to local disk (used when
350 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700351
352 This object is shared among multiple threads running 'retrieve_results'
353 function, in particular they call 'process_shard_result' method in parallel.
354 """
355
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000356 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700357 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
358
359 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700360 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361 shard_count: expected number of task shards.
362 """
maruel12e30012015-10-09 11:55:35 -0700363 self.task_output_dir = (
364 unicode(os.path.abspath(task_output_dir))
365 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000366 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700367 self.shard_count = shard_count
368
369 self._lock = threading.Lock()
370 self._per_shard_results = {}
371 self._storage = None
372
nodire5028a92016-04-29 14:38:21 -0700373 if self.task_output_dir:
374 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700375
Vadim Shtayurab450c602014-05-12 19:23:25 -0700376 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377 """Stores results of a single task shard, fetches output files if necessary.
378
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400379 Modifies |result| in place.
380
maruel77f720b2015-09-15 12:35:22 -0700381 shard_index is 0-based.
382
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700383 Called concurrently from multiple threads.
384 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700386 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 if shard_index < 0 or shard_index >= self.shard_count:
388 logging.warning(
389 'Shard index %d is outside of expected range: [0; %d]',
390 shard_index, self.shard_count - 1)
391 return
392
maruel77f720b2015-09-15 12:35:22 -0700393 if result.get('outputs_ref'):
394 ref = result['outputs_ref']
395 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
396 ref['isolatedserver'],
397 urllib.urlencode(
398 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400399
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400 # Store result dict of that shard, ignore results we've already seen.
401 with self._lock:
402 if shard_index in self._per_shard_results:
403 logging.warning('Ignoring duplicate shard index %d', shard_index)
404 return
405 self._per_shard_results[shard_index] = result
406
407 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700408 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400409 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700410 result['outputs_ref']['isolatedserver'],
411 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400412 if storage:
413 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400414 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
415 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400416 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700417 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400418 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400419 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700420 os.path.join(self.task_output_dir, str(shard_index)),
421 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700422
423 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700424 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700425 with self._lock:
426 # Write an array of shard results with None for missing shards.
427 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700428 'shards': [
429 self._per_shard_results.get(i) for i in xrange(self.shard_count)
430 ],
431 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000432
433 # Don't store stdout in the summary if not requested too.
434 if "json" not in self.task_output_stdout:
435 for shard_json in summary['shards']:
436 if not shard_json:
437 continue
438 if "output" in shard_json:
439 del shard_json["output"]
440 if "outputs" in shard_json:
441 del shard_json["outputs"]
442
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700443 # Write summary.json to task_output_dir as well.
444 if self.task_output_dir:
445 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700446 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700447 summary,
448 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700449 if self._storage:
450 self._storage.close()
451 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700452 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700453
454 def _get_storage(self, isolate_server, namespace):
455 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700456 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700457 with self._lock:
458 if not self._storage:
459 self._storage = isolateserver.get_storage(isolate_server, namespace)
460 else:
461 # Shards must all use exact same isolate server and namespace.
462 if self._storage.location != isolate_server:
463 logging.error(
464 'Task shards are using multiple isolate servers: %s and %s',
465 self._storage.location, isolate_server)
466 return None
467 if self._storage.namespace != namespace:
468 logging.error(
469 'Task shards are using multiple namespaces: %s and %s',
470 self._storage.namespace, namespace)
471 return None
472 return self._storage
473
474
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500475def now():
476 """Exists so it can be mocked easily."""
477 return time.time()
478
479
maruel77f720b2015-09-15 12:35:22 -0700480def parse_time(value):
481 """Converts serialized time from the API to datetime.datetime."""
482 # When microseconds are 0, the '.123456' suffix is elided. This means the
483 # serialized format is not consistent, which confuses the hell out of python.
484 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
485 try:
486 return datetime.datetime.strptime(value, fmt)
487 except ValueError:
488 pass
489 raise ValueError('Failed to parse %s' % value)
490
491
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700492def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700493 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000494 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400495 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700496
Vadim Shtayurab450c602014-05-12 19:23:25 -0700497 Returns:
498 <result dict> on success.
499 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700500 """
maruel71c61c82016-02-22 06:52:05 -0800501 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700502 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700503 if include_perf:
504 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700505 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700506 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400507 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700508 attempt = 0
509
510 while not should_stop.is_set():
511 attempt += 1
512
513 # Waiting for too long -> give up.
514 current_time = now()
515 if deadline and current_time >= deadline:
516 logging.error('retrieve_results(%s) timed out on attempt %d',
517 base_url, attempt)
518 return None
519
520 # Do not spin too fast. Spin faster at the beginning though.
521 # Start with 1 sec delay and for each 30 sec of waiting add another second
522 # of delay, until hitting 15 sec ceiling.
523 if attempt > 1:
524 max_delay = min(15, 1 + (current_time - started) / 30.0)
525 delay = min(max_delay, deadline - current_time) if deadline else max_delay
526 if delay > 0:
527 logging.debug('Waiting %.1f sec before retrying', delay)
528 should_stop.wait(delay)
529 if should_stop.is_set():
530 return None
531
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400532 # Disable internal retries in net.url_read_json, since we are doing retries
533 # ourselves.
534 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700535 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
536 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400537 # Retry on 500s only if no timeout is specified.
538 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400539 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400540 if timeout == -1:
541 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400542 continue
maruel77f720b2015-09-15 12:35:22 -0700543
maruelbf53e042015-12-01 15:00:51 -0800544 if result.get('error'):
545 # An error occurred.
546 if result['error'].get('errors'):
547 for err in result['error']['errors']:
548 logging.warning(
549 'Error while reading task: %s; %s',
550 err.get('message'), err.get('debugInfo'))
551 elif result['error'].get('message'):
552 logging.warning(
553 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400554 if timeout == -1:
555 return result
maruelbf53e042015-12-01 15:00:51 -0800556 continue
557
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400558 # When timeout == -1, always return on first attempt. 500s are already
559 # retried in this case.
560 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000561 if fetch_stdout:
562 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700563 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700564 # Record the result, try to fetch attached output files (if any).
565 if output_collector:
566 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700567 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700568 if result.get('internal_failure'):
569 logging.error('Internal error!')
570 elif result['state'] == 'BOT_DIED':
571 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700572 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000573
574
maruel77f720b2015-09-15 12:35:22 -0700575def convert_to_old_format(result):
576 """Converts the task result data from Endpoints API format to old API format
577 for compatibility.
578
579 This goes into the file generated as --task-summary-json.
580 """
581 # Sets default.
582 result.setdefault('abandoned_ts', None)
583 result.setdefault('bot_id', None)
584 result.setdefault('bot_version', None)
585 result.setdefault('children_task_ids', [])
586 result.setdefault('completed_ts', None)
587 result.setdefault('cost_saved_usd', None)
588 result.setdefault('costs_usd', None)
589 result.setdefault('deduped_from', None)
590 result.setdefault('name', None)
591 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700592 result.setdefault('server_versions', None)
593 result.setdefault('started_ts', None)
594 result.setdefault('tags', None)
595 result.setdefault('user', None)
596
597 # Convertion back to old API.
598 duration = result.pop('duration', None)
599 result['durations'] = [duration] if duration else []
600 exit_code = result.pop('exit_code', None)
601 result['exit_codes'] = [int(exit_code)] if exit_code else []
602 result['id'] = result.pop('task_id')
603 result['isolated_out'] = result.get('outputs_ref', None)
604 output = result.pop('output', None)
605 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700606 # server_version
607 # Endpoints result 'state' as string. For compatibility with old code, convert
608 # to int.
609 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700610 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700611 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700612 if 'bot_dimensions' in result:
613 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700614 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700615 }
616 else:
617 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700618
619
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700620def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400621 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000622 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500623 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700625 Duplicate shards are ignored. Shards are yielded in order of completion.
626 Timed out shards are NOT yielded at all. Caller can compare number of yielded
627 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628
629 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500630 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 +0000631 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500632
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700633 output_collector is an optional instance of TaskOutputCollector that will be
634 used to fetch files produced by a task from isolate server to the local disk.
635
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500636 Yields:
637 (index, result). In particular, 'result' is defined as the
638 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700642 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700643 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700644
maruel@chromium.org0437a732013-08-27 16:05:52 +0000645 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
646 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700647 # Adds a task to the thread pool to call 'retrieve_results' and return
648 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400649 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000651 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400652 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000653 task_id, timeout, should_stop, output_collector, include_perf,
654 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700655
656 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400657 for shard_index, task_id in enumerate(task_ids):
658 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700659
660 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400661 shards_remaining = range(len(task_ids))
662 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700664 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700665 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700666 shard_index, result = results_channel.pull(
667 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700668 except threading_utils.TaskChannel.Timeout:
669 if print_status_updates:
670 print(
671 'Waiting for results from the following shards: %s' %
672 ', '.join(map(str, shards_remaining)))
673 sys.stdout.flush()
674 continue
675 except Exception:
676 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700677
678 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700679 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000680 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500681 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000682 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700683
Vadim Shtayurab450c602014-05-12 19:23:25 -0700684 # Yield back results to the caller.
685 assert shard_index in shards_remaining
686 shards_remaining.remove(shard_index)
687 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700688
maruel@chromium.org0437a732013-08-27 16:05:52 +0000689 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700690 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000691 should_stop.set()
692
693
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000694def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000695 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700696 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400697 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700698 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
699 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400700 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
701 metadata.get('abandoned_ts')):
702 pending = '%.1fs' % (
703 parse_time(metadata['abandoned_ts']) -
704 parse_time(metadata['created_ts'])
705 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400706 else:
707 pending = 'N/A'
708
maruel77f720b2015-09-15 12:35:22 -0700709 if metadata.get('duration') is not None:
710 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400711 else:
712 duration = 'N/A'
713
maruel77f720b2015-09-15 12:35:22 -0700714 if metadata.get('exit_code') is not None:
715 # Integers are encoded as string to not loose precision.
716 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400717 else:
718 exit_code = 'N/A'
719
720 bot_id = metadata.get('bot_id') or 'N/A'
721
maruel77f720b2015-09-15 12:35:22 -0700722 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400723 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000724 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400725 if metadata.get('state') == 'CANCELED':
726 tag_footer2 = ' Pending: %s CANCELED' % pending
727 elif metadata.get('state') == 'EXPIRED':
728 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400729 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400730 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
731 pending, duration, bot_id, exit_code, metadata['state'])
732 else:
733 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
734 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400735
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000736 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
737 dash_pad = '+-%s-+' % ('-' * tag_len)
738 tag_header = '| %s |' % tag_header.ljust(tag_len)
739 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
740 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400741
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000742 if include_stdout:
743 return '\n'.join([
744 dash_pad,
745 tag_header,
746 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400747 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000748 dash_pad,
749 tag_footer1,
750 tag_footer2,
751 dash_pad,
752 ])
753 else:
754 return '\n'.join([
755 dash_pad,
756 tag_header,
757 tag_footer2,
758 dash_pad,
759 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000760
761
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700762def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700763 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000764 task_summary_json, task_output_dir, task_output_stdout,
765 include_perf):
maruela5490782015-09-30 10:56:59 -0700766 """Retrieves results of a Swarming task.
767
768 Returns:
769 process exit code that should be returned to the user.
770 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700771 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000772 output_collector = TaskOutputCollector(
773 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700774
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700775 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700776 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400777 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700778 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400779 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400780 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000781 output_collector, include_perf,
782 (len(task_output_stdout) > 0),
783 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700784 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700785
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400786 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700787 shard_exit_code = metadata.get('exit_code')
788 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700789 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700790 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700791 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400792 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700793 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700794
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700795 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000796 s = decorate_shard_output(
797 swarming, index, metadata,
798 "console" in task_output_stdout).encode(
799 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700800 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400801 if len(seen_shards) < len(task_ids):
802 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700803 else:
maruel77f720b2015-09-15 12:35:22 -0700804 print('%s: %s %s' % (
805 metadata.get('bot_id', 'N/A'),
806 metadata['task_id'],
807 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000808 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700809 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400810 if output:
811 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700812 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700813 summary = output_collector.finalize()
814 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700815 # TODO(maruel): Make this optional.
816 for i in summary['shards']:
817 if i:
818 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700819 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700820
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400821 if decorate and total_duration:
822 print('Total duration: %.1fs' % total_duration)
823
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400824 if len(seen_shards) != len(task_ids):
825 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700826 print >> sys.stderr, ('Results from some shards are missing: %s' %
827 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700828 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700829
maruela5490782015-09-30 10:56:59 -0700830 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000831
832
maruel77f720b2015-09-15 12:35:22 -0700833### API management.
834
835
836class APIError(Exception):
837 pass
838
839
840def endpoints_api_discovery_apis(host):
841 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
842 the APIs exposed by a host.
843
844 https://developers.google.com/discovery/v1/reference/apis/list
845 """
maruel380e3262016-08-31 16:10:06 -0700846 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
847 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700848 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
849 if data is None:
850 raise APIError('Failed to discover APIs on %s' % host)
851 out = {}
852 for api in data['items']:
853 if api['id'] == 'discovery:v1':
854 continue
855 # URL is of the following form:
856 # url = host + (
857 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
858 api_data = net.url_read_json(api['discoveryRestUrl'])
859 if api_data is None:
860 raise APIError('Failed to discover %s on %s' % (api['id'], host))
861 out[api['id']] = api_data
862 return out
863
864
maruelaf6b06c2017-06-08 06:26:53 -0700865def get_yielder(base_url, limit):
866 """Returns the first query and a function that yields following items."""
867 CHUNK_SIZE = 250
868
869 url = base_url
870 if limit:
871 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
872 data = net.url_read_json(url)
873 if data is None:
874 # TODO(maruel): Do basic diagnostic.
875 raise Failure('Failed to access %s' % url)
876 org_cursor = data.pop('cursor', None)
877 org_total = len(data.get('items') or [])
878 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
879 if not org_cursor or not org_total:
880 # This is not an iterable resource.
881 return data, lambda: []
882
883 def yielder():
884 cursor = org_cursor
885 total = org_total
886 # Some items support cursors. Try to get automatically if cursors are needed
887 # by looking at the 'cursor' items.
888 while cursor and (not limit or total < limit):
889 merge_char = '&' if '?' in base_url else '?'
890 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
891 if limit:
892 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
893 new = net.url_read_json(url)
894 if new is None:
895 raise Failure('Failed to access %s' % url)
896 cursor = new.get('cursor')
897 new_items = new.get('items')
898 nb_items = len(new_items or [])
899 total += nb_items
900 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
901 yield new_items
902
903 return data, yielder
904
905
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500906### Commands.
907
908
909def abort_task(_swarming, _manifest):
910 """Given a task manifest that was triggered, aborts its execution."""
911 # TODO(vadimsh): No supported by the server yet.
912
913
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400914def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800915 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500916 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500917 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500918 dest='dimensions', metavar='FOO bar',
919 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500920 parser.add_option_group(parser.filter_group)
921
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400922
maruelaf6b06c2017-06-08 06:26:53 -0700923def process_filter_options(parser, options):
924 for key, value in options.dimensions:
925 if ':' in key:
926 parser.error('--dimension key cannot contain ":"')
927 if key.strip() != key:
928 parser.error('--dimension key has whitespace')
929 if not key:
930 parser.error('--dimension key is empty')
931
932 if value.strip() != value:
933 parser.error('--dimension value has whitespace')
934 if not value:
935 parser.error('--dimension value is empty')
936 options.dimensions.sort()
937
938
Vadim Shtayurab450c602014-05-12 19:23:25 -0700939def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400940 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700941 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700942 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700943 help='Number of shards to trigger and collect.')
944 parser.add_option_group(parser.sharding_group)
945
946
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400947def add_trigger_options(parser):
948 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500949 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400950 add_filter_options(parser)
951
maruel681d6802017-01-17 16:56:03 -0800952 group = optparse.OptionGroup(parser, 'Task properties')
953 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700954 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500955 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800956 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500957 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700958 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800959 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800960 '--env-prefix', default=[], action='append', nargs=2,
961 metavar='VAR local/path',
962 help='Prepend task-relative `local/path` to the task\'s VAR environment '
963 'variable using os-appropriate pathsep character. Can be specified '
964 'multiple times for the same VAR to add multiple paths.')
965 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400966 '--idempotent', action='store_true', default=False,
967 help='When set, the server will actively try to find a previous task '
968 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800969 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700970 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700971 help='The optional path to a file containing the secret_bytes to use with'
972 'this task.')
maruel681d6802017-01-17 16:56:03 -0800973 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700974 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400975 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800976 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700977 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400978 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800979 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500980 '--raw-cmd', action='store_true', default=False,
981 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700982 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800983 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500984 '--relative-cwd',
985 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
986 'requires --raw-cmd')
987 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700988 '--cipd-package', action='append', default=[], metavar='PKG',
989 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700990 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800991 group.add_option(
992 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700993 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800994 help='"<name> <relpath>" items to keep a persistent bot managed cache')
995 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700996 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700997 help='Email of a service account to run the task as, or literal "bot" '
998 'string to indicate that the task should use the same account the '
999 'bot itself is using to authenticate to Swarming. Don\'t use task '
1000 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001001 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001002 '-o', '--output', action='append', default=[], metavar='PATH',
1003 help='A list of files to return in addition to those written to '
1004 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1005 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -08001006 parser.add_option_group(group)
1007
1008 group = optparse.OptionGroup(parser, 'Task request')
1009 group.add_option(
1010 '--priority', type='int', default=100,
1011 help='The lower value, the more important the task is')
1012 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001013 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001014 help='Display name of the task. Defaults to '
1015 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1016 'isolated file is provided, if a hash is provided, it defaults to '
1017 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1018 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001019 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001020 help='Tags to assign to the task.')
1021 group.add_option(
1022 '--user', default='',
1023 help='User associated with the task. Defaults to authenticated user on '
1024 'the server.')
1025 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001026 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001027 help='Seconds to allow the task to be pending for a bot to run before '
1028 'this task request expires.')
1029 group.add_option(
1030 '--deadline', type='int', dest='expiration',
1031 help=optparse.SUPPRESS_HELP)
1032 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001033
1034
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001035def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001036 """Processes trigger options and does preparatory steps.
1037
1038 Returns:
1039 NewTaskRequest instance.
1040 """
maruelaf6b06c2017-06-08 06:26:53 -07001041 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001042 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001043 if args and args[0] == '--':
1044 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001045
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001046 if not options.dimensions:
1047 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001048 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1049 parser.error('--tags must be in the format key:value')
1050 if options.raw_cmd and not args:
1051 parser.error(
1052 'Arguments with --raw-cmd should be passed after -- as command '
1053 'delimiter.')
1054 if options.isolate_server and not options.namespace:
1055 parser.error(
1056 '--namespace must be a valid value when --isolate-server is used')
1057 if not options.isolated and not options.raw_cmd:
1058 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1059
1060 # Isolated
1061 # --isolated is required only if --raw-cmd wasn't provided.
1062 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1063 # preferred server.
1064 isolateserver.process_isolate_server_options(
1065 parser, options, False, not options.raw_cmd)
1066 inputs_ref = None
1067 if options.isolate_server:
1068 inputs_ref = FilesRef(
1069 isolated=options.isolated,
1070 isolatedserver=options.isolate_server,
1071 namespace=options.namespace)
1072
1073 # Command
1074 command = None
1075 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001077 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001078 if options.relative_cwd:
1079 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1080 if not a.startswith(os.getcwd()):
1081 parser.error(
1082 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001083 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001084 if options.relative_cwd:
1085 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001086 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001087
maruel0a25f6c2017-05-10 10:43:23 -07001088 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001089 cipd_packages = []
1090 for p in options.cipd_package:
1091 split = p.split(':', 2)
1092 if len(split) != 3:
1093 parser.error('CIPD packages must take the form: path:package:version')
1094 cipd_packages.append(CipdPackage(
1095 package_name=split[1],
1096 path=split[0],
1097 version=split[2]))
1098 cipd_input = None
1099 if cipd_packages:
1100 cipd_input = CipdInput(
1101 client_package=None,
1102 packages=cipd_packages,
1103 server=None)
1104
maruel0a25f6c2017-05-10 10:43:23 -07001105 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001106 secret_bytes = None
1107 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001108 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001109 secret_bytes = f.read().encode('base64')
1110
maruel0a25f6c2017-05-10 10:43:23 -07001111 # Named caches
maruel681d6802017-01-17 16:56:03 -08001112 caches = [
1113 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1114 for i in options.named_cache
1115 ]
maruel0a25f6c2017-05-10 10:43:23 -07001116
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001117 env_prefixes = {}
1118 for k, v in options.env_prefix:
1119 env_prefixes.setdefault(k, []).append(v)
1120
maruel77f720b2015-09-15 12:35:22 -07001121 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001122 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001123 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001124 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001125 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001126 dimensions=options.dimensions,
1127 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001128 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001129 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001130 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001131 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001132 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001133 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001134 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001135 outputs=options.output,
1136 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001137
maruel77f720b2015-09-15 12:35:22 -07001138 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001139 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001140 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001141 priority=options.priority,
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001142 task_slices=[
1143 {
1144 'expiration_secs': options.expiration,
1145 'properties': properties,
1146 },
1147 ],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001148 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001149 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001150 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001151
1152
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001153class TaskOutputStdoutOption(optparse.Option):
1154 """Where to output the each task's console output (stderr/stdout).
1155
1156 The output will be;
1157 none - not be downloaded.
1158 json - stored in summary.json file *only*.
1159 console - shown on stdout *only*.
1160 all - stored in summary.json and shown on stdout.
1161 """
1162
1163 choices = ['all', 'json', 'console', 'none']
1164
1165 def __init__(self, *args, **kw):
1166 optparse.Option.__init__(
1167 self,
1168 *args,
1169 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001170 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001171 help=re.sub('\s\s*', ' ', self.__doc__),
1172 **kw)
1173
1174 def convert_value(self, opt, value):
1175 if value not in self.choices:
1176 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1177 self.get_opt_string(), self.choices, value))
1178 stdout_to = []
1179 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001180 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001181 elif value != 'none':
1182 stdout_to = [value]
1183 return stdout_to
1184
1185
maruel@chromium.org0437a732013-08-27 16:05:52 +00001186def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001187 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001188 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001189 help='Timeout to wait for result, set to -1 for no timeout and get '
1190 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001191 parser.group_logging.add_option(
1192 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001193 parser.group_logging.add_option(
1194 '--print-status-updates', action='store_true',
1195 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001196 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001197 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001198 '--task-summary-json',
1199 metavar='FILE',
1200 help='Dump a summary of task results to this file as json. It contains '
1201 'only shards statuses as know to server directly. Any output files '
1202 'emitted by the task can be collected by using --task-output-dir')
1203 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001204 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001205 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001206 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001207 'directory contains per-shard directory with output files produced '
1208 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001209 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001210 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001211 parser.task_output_group.add_option(
1212 '--perf', action='store_true', default=False,
1213 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001214 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001215
1216
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001217def process_collect_options(parser, options):
1218 # Only negative -1 is allowed, disallow other negative values.
1219 if options.timeout != -1 and options.timeout < 0:
1220 parser.error('Invalid --timeout value')
1221
1222
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001223@subcommand.usage('bots...')
1224def CMDbot_delete(parser, args):
1225 """Forcibly deletes bots from the Swarming server."""
1226 parser.add_option(
1227 '-f', '--force', action='store_true',
1228 help='Do not prompt for confirmation')
1229 options, args = parser.parse_args(args)
1230 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001231 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001232
1233 bots = sorted(args)
1234 if not options.force:
1235 print('Delete the following bots?')
1236 for bot in bots:
1237 print(' %s' % bot)
1238 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1239 print('Goodbye.')
1240 return 1
1241
1242 result = 0
1243 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001244 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001245 if net.url_read_json(url, data={}, method='POST') is None:
1246 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001247 result = 1
1248 return result
1249
1250
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001251def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001252 """Returns information about the bots connected to the Swarming server."""
1253 add_filter_options(parser)
1254 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001255 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001256 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001257 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001258 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001259 help='Keep both dead and alive bots')
1260 parser.filter_group.add_option(
1261 '--busy', action='store_true', help='Keep only busy bots')
1262 parser.filter_group.add_option(
1263 '--idle', action='store_true', help='Keep only idle bots')
1264 parser.filter_group.add_option(
1265 '--mp', action='store_true',
1266 help='Keep only Machine Provider managed bots')
1267 parser.filter_group.add_option(
1268 '--non-mp', action='store_true',
1269 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001270 parser.filter_group.add_option(
1271 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001272 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001273 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001274 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001275
1276 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001277 parser.error('Use only one of --keep-dead or --dead-only')
1278 if options.busy and options.idle:
1279 parser.error('Use only one of --busy or --idle')
1280 if options.mp and options.non_mp:
1281 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001282
maruelaf6b06c2017-06-08 06:26:53 -07001283 url = options.swarming + '/api/swarming/v1/bots/list?'
1284 values = []
1285 if options.dead_only:
1286 values.append(('is_dead', 'TRUE'))
1287 elif options.keep_dead:
1288 values.append(('is_dead', 'NONE'))
1289 else:
1290 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001291
maruelaf6b06c2017-06-08 06:26:53 -07001292 if options.busy:
1293 values.append(('is_busy', 'TRUE'))
1294 elif options.idle:
1295 values.append(('is_busy', 'FALSE'))
1296 else:
1297 values.append(('is_busy', 'NONE'))
1298
1299 if options.mp:
1300 values.append(('is_mp', 'TRUE'))
1301 elif options.non_mp:
1302 values.append(('is_mp', 'FALSE'))
1303 else:
1304 values.append(('is_mp', 'NONE'))
1305
1306 for key, value in options.dimensions:
1307 values.append(('dimensions', '%s:%s' % (key, value)))
1308 url += urllib.urlencode(values)
1309 try:
1310 data, yielder = get_yielder(url, 0)
1311 bots = data.get('items') or []
1312 for items in yielder():
1313 if items:
1314 bots.extend(items)
1315 except Failure as e:
1316 sys.stderr.write('\n%s\n' % e)
1317 return 1
maruel77f720b2015-09-15 12:35:22 -07001318 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001319 print bot['bot_id']
1320 if not options.bare:
1321 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1322 print ' %s' % json.dumps(dimensions, sort_keys=True)
1323 if bot.get('task_id'):
1324 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001325 return 0
1326
1327
maruelfd0a90c2016-06-10 11:51:10 -07001328@subcommand.usage('task_id')
1329def CMDcancel(parser, args):
1330 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001331 parser.add_option(
1332 '-k', '--kill-running', action='store_true', default=False,
1333 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001334 options, args = parser.parse_args(args)
1335 if not args:
1336 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001337 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001338 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001339 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001340 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001341 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001342 print('Deleting %s failed. Probably already gone' % task_id)
1343 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001344 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001345 return 0
1346
1347
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001348@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001349def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001350 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001351
1352 The result can be in multiple part if the execution was sharded. It can
1353 potentially have retries.
1354 """
1355 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001356 parser.add_option(
1357 '-j', '--json',
1358 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001359 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001360 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001361 if not args and not options.json:
1362 parser.error('Must specify at least one task id or --json.')
1363 if args and options.json:
1364 parser.error('Only use one of task id or --json.')
1365
1366 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001367 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001368 try:
maruel1ceb3872015-10-14 06:10:44 -07001369 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001370 data = json.load(f)
1371 except (IOError, ValueError):
1372 parser.error('Failed to open %s' % options.json)
1373 try:
1374 tasks = sorted(
1375 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1376 args = [t['task_id'] for t in tasks]
1377 except (KeyError, TypeError):
1378 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001379 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001380 # Take in account all the task slices.
1381 offset = 0
1382 for s in data['request']['task_slices']:
1383 m = (offset + s['properties']['execution_timeout_secs'] +
1384 s['expiration_secs'])
1385 if m > options.timeout:
1386 options.timeout = m
1387 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001388 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001389 else:
1390 valid = frozenset('0123456789abcdef')
1391 if any(not valid.issuperset(task_id) for task_id in args):
1392 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001393
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001394 try:
1395 return collect(
1396 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001397 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001398 options.timeout,
1399 options.decorate,
1400 options.print_status_updates,
1401 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001402 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001403 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001404 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001405 except Failure:
1406 on_error.report(None)
1407 return 1
1408
1409
maruel77f720b2015-09-15 12:35:22 -07001410@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001411def CMDpost(parser, args):
1412 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1413
1414 Input data must be sent to stdin, result is printed to stdout.
1415
1416 If HTTP response code >= 400, returns non-zero.
1417 """
1418 options, args = parser.parse_args(args)
1419 if len(args) != 1:
1420 parser.error('Must specify only API name')
1421 url = options.swarming + '/api/swarming/v1/' + args[0]
1422 data = sys.stdin.read()
1423 try:
1424 resp = net.url_read(url, data=data, method='POST')
1425 except net.TimeoutError:
1426 sys.stderr.write('Timeout!\n')
1427 return 1
1428 if not resp:
1429 sys.stderr.write('No response!\n')
1430 return 1
1431 sys.stdout.write(resp)
1432 return 0
1433
1434
1435@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001436def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001437 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1438 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001439
1440 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001441 Raw task request and results:
1442 swarming.py query -S server-url.com task/123456/request
1443 swarming.py query -S server-url.com task/123456/result
1444
maruel77f720b2015-09-15 12:35:22 -07001445 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001446 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001447
maruelaf6b06c2017-06-08 06:26:53 -07001448 Listing last 10 tasks on a specific bot named 'bot1':
1449 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001450
maruelaf6b06c2017-06-08 06:26:53 -07001451 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001452 quoting is important!:
1453 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001454 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001455 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001456 parser.add_option(
1457 '-L', '--limit', type='int', default=200,
1458 help='Limit to enforce on limitless items (like number of tasks); '
1459 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001460 parser.add_option(
1461 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001462 parser.add_option(
1463 '--progress', action='store_true',
1464 help='Prints a dot at each request to show progress')
1465 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001466 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001467 parser.error(
1468 'Must specify only method name and optionally query args properly '
1469 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001470 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001471 try:
1472 data, yielder = get_yielder(base_url, options.limit)
1473 for items in yielder():
1474 if items:
1475 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001476 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001477 sys.stderr.write('.')
1478 sys.stderr.flush()
1479 except Failure as e:
1480 sys.stderr.write('\n%s\n' % e)
1481 return 1
maruel77f720b2015-09-15 12:35:22 -07001482 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001483 sys.stderr.write('\n')
1484 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001485 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001486 options.json = unicode(os.path.abspath(options.json))
1487 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001488 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001489 try:
maruel77f720b2015-09-15 12:35:22 -07001490 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001491 sys.stdout.write('\n')
1492 except IOError:
1493 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001494 return 0
1495
1496
maruel77f720b2015-09-15 12:35:22 -07001497def CMDquery_list(parser, args):
1498 """Returns list of all the Swarming APIs that can be used with command
1499 'query'.
1500 """
1501 parser.add_option(
1502 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1503 options, args = parser.parse_args(args)
1504 if args:
1505 parser.error('No argument allowed.')
1506
1507 try:
1508 apis = endpoints_api_discovery_apis(options.swarming)
1509 except APIError as e:
1510 parser.error(str(e))
1511 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001512 options.json = unicode(os.path.abspath(options.json))
1513 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001514 json.dump(apis, f)
1515 else:
1516 help_url = (
1517 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1518 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001519 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1520 if i:
1521 print('')
maruel77f720b2015-09-15 12:35:22 -07001522 print api_id
maruel11e31af2017-02-15 07:30:50 -08001523 print ' ' + api['description'].strip()
1524 if 'resources' in api:
1525 # Old.
1526 for j, (resource_name, resource) in enumerate(
1527 sorted(api['resources'].iteritems())):
1528 if j:
1529 print('')
1530 for method_name, method in sorted(resource['methods'].iteritems()):
1531 # Only list the GET ones.
1532 if method['httpMethod'] != 'GET':
1533 continue
1534 print '- %s.%s: %s' % (
1535 resource_name, method_name, method['path'])
1536 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001537 ' ' + l for l in textwrap.wrap(
1538 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001539 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1540 else:
1541 # New.
1542 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001543 # Only list the GET ones.
1544 if method['httpMethod'] != 'GET':
1545 continue
maruel11e31af2017-02-15 07:30:50 -08001546 print '- %s: %s' % (method['id'], method['path'])
1547 print('\n'.join(
1548 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001549 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1550 return 0
1551
1552
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001553@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001554def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001555 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001556
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001557 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001558 """
1559 add_trigger_options(parser)
1560 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001561 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001562 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001563 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001564 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001565 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001566 tasks = trigger_task_shards(
1567 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001568 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001569 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001570 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001571 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001572 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001573 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001574 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001575 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001576 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001577 task_ids = [
1578 t['task_id']
1579 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1580 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001581 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001582 offset = 0
1583 for s in task_request.task_slices:
1584 m = (offset + s['properties'].execution_timeout_secs +
1585 s['expiration_secs'])
1586 if m > options.timeout:
1587 options.timeout = m
1588 offset += s['expiration_secs']
1589 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001590 try:
1591 return collect(
1592 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001593 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001594 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001595 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001596 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001597 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001598 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001599 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001600 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001601 except Failure:
1602 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001603 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001604
1605
maruel18122c62015-10-23 06:31:23 -07001606@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001607def CMDreproduce(parser, args):
1608 """Runs a task locally that was triggered on the server.
1609
1610 This running locally the same commands that have been run on the bot. The data
1611 downloaded will be in a subdirectory named 'work' of the current working
1612 directory.
maruel18122c62015-10-23 06:31:23 -07001613
1614 You can pass further additional arguments to the target command by passing
1615 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001616 """
maruelc070e672016-02-22 17:32:57 -08001617 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001618 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001619 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001620 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001621 extra_args = []
1622 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001623 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001624 if len(args) > 1:
1625 if args[1] == '--':
1626 if len(args) > 2:
1627 extra_args = args[2:]
1628 else:
1629 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001630
maruel380e3262016-08-31 16:10:06 -07001631 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001632 request = net.url_read_json(url)
1633 if not request:
1634 print >> sys.stderr, 'Failed to retrieve request data for the task'
1635 return 1
1636
maruel12e30012015-10-09 11:55:35 -07001637 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001638 if fs.isdir(workdir):
1639 parser.error('Please delete the directory \'work\' first')
1640 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001641 cachedir = unicode(os.path.abspath('cipd_cache'))
1642 if not fs.exists(cachedir):
1643 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001644
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001645 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001646 env = os.environ.copy()
1647 env['SWARMING_BOT_ID'] = 'reproduce'
1648 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001649 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001650 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001651 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001652 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001653 if not i['value']:
1654 env.pop(key, None)
1655 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001656 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001657
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001658 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001659 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001660 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001661 for i in env_prefixes:
1662 key = i['key']
1663 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001664 cur = env.get(key)
1665 if cur:
1666 paths.append(cur)
1667 env[key] = os.path.pathsep.join(paths)
1668
iannucci31ab9192017-05-02 19:11:56 -07001669 command = []
nodir152cba62016-05-12 16:08:56 -07001670 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001671 # Create the tree.
1672 with isolateserver.get_storage(
1673 properties['inputs_ref']['isolatedserver'],
1674 properties['inputs_ref']['namespace']) as storage:
1675 bundle = isolateserver.fetch_isolated(
1676 properties['inputs_ref']['isolated'],
1677 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -04001678 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001679 workdir,
1680 False)
maruel29ab2fd2015-10-16 11:44:01 -07001681 command = bundle.command
1682 if bundle.relative_cwd:
1683 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001684 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001685
1686 if properties.get('command'):
1687 command.extend(properties['command'])
1688
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001689 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001690 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001691 if not options.output_dir:
1692 new_command = run_isolated.process_command(command, 'invalid', None)
1693 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001694 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001695 else:
1696 # Make the path absolute, as the process will run from a subdirectory.
1697 options.output_dir = os.path.abspath(options.output_dir)
1698 new_command = run_isolated.process_command(
1699 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001700 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001701 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001702 command = new_command
1703 file_path.ensure_command_has_abs_path(command, workdir)
1704
1705 if properties.get('cipd_input'):
1706 ci = properties['cipd_input']
1707 cp = ci['client_package']
1708 client_manager = cipd.get_client(
1709 ci['server'], cp['package_name'], cp['version'], cachedir)
1710
1711 with client_manager as client:
1712 by_path = collections.defaultdict(list)
1713 for pkg in ci['packages']:
1714 path = pkg['path']
1715 # cipd deals with 'root' as ''
1716 if path == '.':
1717 path = ''
1718 by_path[path].append((pkg['package_name'], pkg['version']))
1719 client.ensure(workdir, by_path, cache_dir=cachedir)
1720
maruel77f720b2015-09-15 12:35:22 -07001721 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001722 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001723 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001724 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001725 print >> sys.stderr, str(e)
1726 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001727
1728
maruel0eb1d1b2015-10-02 14:48:21 -07001729@subcommand.usage('bot_id')
1730def CMDterminate(parser, args):
1731 """Tells a bot to gracefully shut itself down as soon as it can.
1732
1733 This is done by completing whatever current task there is then exiting the bot
1734 process.
1735 """
1736 parser.add_option(
1737 '--wait', action='store_true', help='Wait for the bot to terminate')
1738 options, args = parser.parse_args(args)
1739 if len(args) != 1:
1740 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001741 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001742 request = net.url_read_json(url, data={})
1743 if not request:
1744 print >> sys.stderr, 'Failed to ask for termination'
1745 return 1
1746 if options.wait:
1747 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001748 options.swarming,
1749 [request['task_id']],
1750 0.,
1751 False,
1752 False,
1753 None,
1754 None,
1755 [],
maruel9531ce02016-04-13 06:11:23 -07001756 False)
maruelbfc5f872017-06-10 16:43:17 -07001757 else:
1758 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001759 return 0
1760
1761
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001762@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001763def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001764 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001765
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001766 Passes all extra arguments provided after '--' as additional command line
1767 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001768 """
1769 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001770 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001771 parser.add_option(
1772 '--dump-json',
1773 metavar='FILE',
1774 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001775 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001776 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001777 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001778 tasks = trigger_task_shards(
1779 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001780 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001781 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001782 tasks_sorted = sorted(
1783 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001784 if options.dump_json:
1785 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001786 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001787 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001788 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001789 }
maruel46b015f2015-10-13 18:40:35 -07001790 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001791 print('To collect results, use:')
1792 print(' swarming.py collect -S %s --json %s' %
1793 (options.swarming, options.dump_json))
1794 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001795 print('To collect results, use:')
1796 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001797 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1798 print('Or visit:')
1799 for t in tasks_sorted:
1800 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001801 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001802 except Failure:
1803 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001804 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001805
1806
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001807class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001808 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001809 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001810 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001811 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001812 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001813 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001814 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001815 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001816 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001817 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001818
1819 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001820 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001821 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001822 auth.process_auth_options(self, options)
1823 user = self._process_swarming(options)
1824 if hasattr(options, 'user') and not options.user:
1825 options.user = user
1826 return options, args
1827
1828 def _process_swarming(self, options):
1829 """Processes the --swarming option and aborts if not specified.
1830
1831 Returns the identity as determined by the server.
1832 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001833 if not options.swarming:
1834 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001835 try:
1836 options.swarming = net.fix_url(options.swarming)
1837 except ValueError as e:
1838 self.error('--swarming %s' % e)
1839 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001840 try:
1841 user = auth.ensure_logged_in(options.swarming)
1842 except ValueError as e:
1843 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001844 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001845
1846
1847def main(args):
1848 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001849 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001850
1851
1852if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001853 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001854 fix_encoding.fix_encoding()
1855 tools.disable_buffering()
1856 colorama.init()
1857 sys.exit(main(sys.argv[1:]))