blob: d53c8423637eb2322d1e60b7c773362678b97086 [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 Ruelba1bf222017-12-21 21:41:01 -05008__version__ = '0.10.1'
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 subprocess
18import sys
maruel11e31af2017-02-15 07:30:50 -080019import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070020import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000021import time
22import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000023
24from third_party import colorama
25from third_party.depot_tools import fix_encoding
26from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000027
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050028from utils import file_path
maruel12e30012015-10-09 11:55:35 -070029from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040030from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040031from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000032from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040033from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070034from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000035from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000036from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000037
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080038import auth
iannucci31ab9192017-05-02 19:11:56 -070039import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000040import isolateserver
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 [
133 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500134 'name',
maruel77f720b2015-09-15 12:35:22 -0700135 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500136 'priority',
maruel77f720b2015-09-15 12:35:22 -0700137 'properties',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700138 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500139 'tags',
140 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500141 ])
142
143
maruel77f720b2015-09-15 12:35:22 -0700144def namedtuple_to_dict(value):
145 """Recursively converts a namedtuple to a dict."""
146 out = dict(value._asdict())
147 for k, v in out.iteritems():
148 if hasattr(v, '_asdict'):
149 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700150 elif isinstance(v, (list, tuple)):
151 l = []
152 for elem in v:
153 if hasattr(elem, '_asdict'):
154 l.append(namedtuple_to_dict(elem))
155 else:
156 l.append(elem)
157 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700158 return out
159
160
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700161def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800162 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700163
164 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500165 """
maruel77f720b2015-09-15 12:35:22 -0700166 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700167 # Don't send 'service_account' if it is None to avoid confusing older
168 # version of the server that doesn't know about 'service_account' and don't
169 # use it at all.
170 if not out['service_account']:
171 out.pop('service_account')
maruel77f720b2015-09-15 12:35:22 -0700172 out['properties']['dimensions'] = [
173 {'key': k, 'value': v}
maruelaf6b06c2017-06-08 06:26:53 -0700174 for k, v in out['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700175 ]
maruel77f720b2015-09-15 12:35:22 -0700176 out['properties']['env'] = [
177 {'key': k, 'value': v}
178 for k, v in out['properties']['env'].iteritems()
179 ]
180 out['properties']['env'].sort(key=lambda x: x['key'])
181 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500182
183
maruel77f720b2015-09-15 12:35:22 -0700184def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500185 """Triggers a request on the Swarming server and returns the json data.
186
187 It's the low-level function.
188
189 Returns:
190 {
191 'request': {
192 'created_ts': u'2010-01-02 03:04:05',
193 'name': ..
194 },
195 'task_id': '12300',
196 }
197 """
198 logging.info('Triggering: %s', raw_request['name'])
199
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500200 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700201 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500202 if not result:
203 on_error.report('Failed to trigger task %s' % raw_request['name'])
204 return None
maruele557bce2015-11-17 09:01:27 -0800205 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800206 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800207 msg = 'Failed to trigger task %s' % raw_request['name']
208 if result['error'].get('errors'):
209 for err in result['error']['errors']:
210 if err.get('message'):
211 msg += '\nMessage: %s' % err['message']
212 if err.get('debugInfo'):
213 msg += '\nDebug info:\n%s' % err['debugInfo']
214 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800215 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800216
217 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800218 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500219 return result
220
221
222def setup_googletest(env, shards, index):
223 """Sets googletest specific environment variables."""
224 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700225 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
226 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
227 env = env[:]
228 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
229 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500230 return env
231
232
233def trigger_task_shards(swarming, task_request, shards):
234 """Triggers one or many subtasks of a sharded task.
235
236 Returns:
237 Dict with task details, returned to caller as part of --dump-json output.
238 None in case of failure.
239 """
240 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700241 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700243 req['properties']['env'] = setup_googletest(
244 req['properties']['env'], shards, index)
245 req['name'] += ':%s:%s' % (index, shards)
246 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500247
248 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500249 tasks = {}
250 priority_warning = False
251 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700252 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500253 if not task:
254 break
255 logging.info('Request result: %s', task)
256 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400257 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500258 priority_warning = True
259 print >> sys.stderr, (
260 'Priority was reset to %s' % task['request']['priority'])
261 tasks[request['name']] = {
262 'shard_index': index,
263 'task_id': task['task_id'],
264 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
265 }
266
267 # Some shards weren't triggered. Abort everything.
268 if len(tasks) != len(requests):
269 if tasks:
270 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
271 len(tasks), len(requests))
272 for task_dict in tasks.itervalues():
273 abort_task(swarming, task_dict['task_id'])
274 return None
275
276 return tasks
277
278
279### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000280
281
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700282# How often to print status updates to stdout in 'collect'.
283STATUS_UPDATE_INTERVAL = 15 * 60.
284
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400285
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400286class State(object):
287 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000288
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400289 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
290 values are part of the API so if they change, the API changed.
291
292 It's in fact an enum. Values should be in decreasing order of importance.
293 """
294 RUNNING = 0x10
295 PENDING = 0x20
296 EXPIRED = 0x30
297 TIMED_OUT = 0x40
298 BOT_DIED = 0x50
299 CANCELED = 0x60
300 COMPLETED = 0x70
301
maruel77f720b2015-09-15 12:35:22 -0700302 STATES = (
303 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
304 'COMPLETED')
305 STATES_RUNNING = ('RUNNING', 'PENDING')
306 STATES_NOT_RUNNING = (
307 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
308 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
309 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400310
311 _NAMES = {
312 RUNNING: 'Running',
313 PENDING: 'Pending',
314 EXPIRED: 'Expired',
315 TIMED_OUT: 'Execution timed out',
316 BOT_DIED: 'Bot died',
317 CANCELED: 'User canceled',
318 COMPLETED: 'Completed',
319 }
320
maruel77f720b2015-09-15 12:35:22 -0700321 _ENUMS = {
322 'RUNNING': RUNNING,
323 'PENDING': PENDING,
324 'EXPIRED': EXPIRED,
325 'TIMED_OUT': TIMED_OUT,
326 'BOT_DIED': BOT_DIED,
327 'CANCELED': CANCELED,
328 'COMPLETED': COMPLETED,
329 }
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
414 # tasks. So use MemoryCache for them instead of on-disk cache. Make
415 # files writable, so that calling script can delete them.
416 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700417 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400418 storage,
419 isolateserver.MemoryCache(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()
507 deadline = started + timeout if timeout else None
508 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 Ruel12a7da42014-10-01 08:29:47 -0400537 result = net.url_read_json(result_url, retry_50x=False)
538 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400539 continue
maruel77f720b2015-09-15 12:35:22 -0700540
maruelbf53e042015-12-01 15:00:51 -0800541 if result.get('error'):
542 # An error occurred.
543 if result['error'].get('errors'):
544 for err in result['error']['errors']:
545 logging.warning(
546 'Error while reading task: %s; %s',
547 err.get('message'), err.get('debugInfo'))
548 elif result['error'].get('message'):
549 logging.warning(
550 'Error while reading task: %s', result['error']['message'])
551 continue
552
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400553 if result['state'] in State.STATES_NOT_RUNNING:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000554 if fetch_stdout:
555 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700556 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700557 # Record the result, try to fetch attached output files (if any).
558 if output_collector:
559 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700560 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700561 if result.get('internal_failure'):
562 logging.error('Internal error!')
563 elif result['state'] == 'BOT_DIED':
564 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700565 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000566
567
maruel77f720b2015-09-15 12:35:22 -0700568def convert_to_old_format(result):
569 """Converts the task result data from Endpoints API format to old API format
570 for compatibility.
571
572 This goes into the file generated as --task-summary-json.
573 """
574 # Sets default.
575 result.setdefault('abandoned_ts', None)
576 result.setdefault('bot_id', None)
577 result.setdefault('bot_version', None)
578 result.setdefault('children_task_ids', [])
579 result.setdefault('completed_ts', None)
580 result.setdefault('cost_saved_usd', None)
581 result.setdefault('costs_usd', None)
582 result.setdefault('deduped_from', None)
583 result.setdefault('name', None)
584 result.setdefault('outputs_ref', None)
585 result.setdefault('properties_hash', None)
586 result.setdefault('server_versions', None)
587 result.setdefault('started_ts', None)
588 result.setdefault('tags', None)
589 result.setdefault('user', None)
590
591 # Convertion back to old API.
592 duration = result.pop('duration', None)
593 result['durations'] = [duration] if duration else []
594 exit_code = result.pop('exit_code', None)
595 result['exit_codes'] = [int(exit_code)] if exit_code else []
596 result['id'] = result.pop('task_id')
597 result['isolated_out'] = result.get('outputs_ref', None)
598 output = result.pop('output', None)
599 result['outputs'] = [output] if output else []
600 # properties_hash
601 # server_version
602 # Endpoints result 'state' as string. For compatibility with old code, convert
603 # to int.
604 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700605 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700606 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700607 if 'bot_dimensions' in result:
608 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700609 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700610 }
611 else:
612 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700613
614
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700615def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400616 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000617 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500618 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000619
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700620 Duplicate shards are ignored. Shards are yielded in order of completion.
621 Timed out shards are NOT yielded at all. Caller can compare number of yielded
622 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000623
624 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500625 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 +0000626 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500627
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700628 output_collector is an optional instance of TaskOutputCollector that will be
629 used to fetch files produced by a task from isolate server to the local disk.
630
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500631 Yields:
632 (index, result). In particular, 'result' is defined as the
633 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000634 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000635 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400636 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700637 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700638 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700639
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
641 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700642 # Adds a task to the thread pool to call 'retrieve_results' and return
643 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400644 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700645 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000646 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400647 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000648 task_id, timeout, should_stop, output_collector, include_perf,
649 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650
651 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400652 for shard_index, task_id in enumerate(task_ids):
653 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700654
655 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400656 shards_remaining = range(len(task_ids))
657 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700658 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700659 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700661 shard_index, result = results_channel.pull(
662 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 except threading_utils.TaskChannel.Timeout:
664 if print_status_updates:
665 print(
666 'Waiting for results from the following shards: %s' %
667 ', '.join(map(str, shards_remaining)))
668 sys.stdout.flush()
669 continue
670 except Exception:
671 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700672
673 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500676 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700678
Vadim Shtayurab450c602014-05-12 19:23:25 -0700679 # Yield back results to the caller.
680 assert shard_index in shards_remaining
681 shards_remaining.remove(shard_index)
682 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700683
maruel@chromium.org0437a732013-08-27 16:05:52 +0000684 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700685 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000686 should_stop.set()
687
688
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000689def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700691 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400692 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700693 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
694 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400695 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
696 metadata.get('abandoned_ts')):
697 pending = '%.1fs' % (
698 parse_time(metadata['abandoned_ts']) -
699 parse_time(metadata['created_ts'])
700 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400701 else:
702 pending = 'N/A'
703
maruel77f720b2015-09-15 12:35:22 -0700704 if metadata.get('duration') is not None:
705 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400706 else:
707 duration = 'N/A'
708
maruel77f720b2015-09-15 12:35:22 -0700709 if metadata.get('exit_code') is not None:
710 # Integers are encoded as string to not loose precision.
711 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400712 else:
713 exit_code = 'N/A'
714
715 bot_id = metadata.get('bot_id') or 'N/A'
716
maruel77f720b2015-09-15 12:35:22 -0700717 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400718 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000719 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400720 if metadata.get('state') == 'CANCELED':
721 tag_footer2 = ' Pending: %s CANCELED' % pending
722 elif metadata.get('state') == 'EXPIRED':
723 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
724 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT'):
725 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
726 pending, duration, bot_id, exit_code, metadata['state'])
727 else:
728 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
729 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400730
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000731 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
732 dash_pad = '+-%s-+' % ('-' * tag_len)
733 tag_header = '| %s |' % tag_header.ljust(tag_len)
734 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
735 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400736
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000737 if include_stdout:
738 return '\n'.join([
739 dash_pad,
740 tag_header,
741 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400742 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000743 dash_pad,
744 tag_footer1,
745 tag_footer2,
746 dash_pad,
747 ])
748 else:
749 return '\n'.join([
750 dash_pad,
751 tag_header,
752 tag_footer2,
753 dash_pad,
754 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000755
756
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700757def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700758 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000759 task_summary_json, task_output_dir, task_output_stdout,
760 include_perf):
maruela5490782015-09-30 10:56:59 -0700761 """Retrieves results of a Swarming task.
762
763 Returns:
764 process exit code that should be returned to the user.
765 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700766 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000767 output_collector = TaskOutputCollector(
768 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700770 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700771 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400772 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400774 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400775 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000776 output_collector, include_perf,
777 (len(task_output_stdout) > 0),
778 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700779 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700780
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400781 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700782 shard_exit_code = metadata.get('exit_code')
783 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700784 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700785 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700786 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400787 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700788 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700789
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000791 s = decorate_shard_output(
792 swarming, index, metadata,
793 "console" in task_output_stdout).encode(
794 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700795 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400796 if len(seen_shards) < len(task_ids):
797 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700798 else:
maruel77f720b2015-09-15 12:35:22 -0700799 print('%s: %s %s' % (
800 metadata.get('bot_id', 'N/A'),
801 metadata['task_id'],
802 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000803 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700804 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400805 if output:
806 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700807 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700808 summary = output_collector.finalize()
809 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700810 # TODO(maruel): Make this optional.
811 for i in summary['shards']:
812 if i:
813 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700814 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700815
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400816 if decorate and total_duration:
817 print('Total duration: %.1fs' % total_duration)
818
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400819 if len(seen_shards) != len(task_ids):
820 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700821 print >> sys.stderr, ('Results from some shards are missing: %s' %
822 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700823 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700824
maruela5490782015-09-30 10:56:59 -0700825 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000826
827
maruel77f720b2015-09-15 12:35:22 -0700828### API management.
829
830
831class APIError(Exception):
832 pass
833
834
835def endpoints_api_discovery_apis(host):
836 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
837 the APIs exposed by a host.
838
839 https://developers.google.com/discovery/v1/reference/apis/list
840 """
maruel380e3262016-08-31 16:10:06 -0700841 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
842 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700843 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
844 if data is None:
845 raise APIError('Failed to discover APIs on %s' % host)
846 out = {}
847 for api in data['items']:
848 if api['id'] == 'discovery:v1':
849 continue
850 # URL is of the following form:
851 # url = host + (
852 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
853 api_data = net.url_read_json(api['discoveryRestUrl'])
854 if api_data is None:
855 raise APIError('Failed to discover %s on %s' % (api['id'], host))
856 out[api['id']] = api_data
857 return out
858
859
maruelaf6b06c2017-06-08 06:26:53 -0700860def get_yielder(base_url, limit):
861 """Returns the first query and a function that yields following items."""
862 CHUNK_SIZE = 250
863
864 url = base_url
865 if limit:
866 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
867 data = net.url_read_json(url)
868 if data is None:
869 # TODO(maruel): Do basic diagnostic.
870 raise Failure('Failed to access %s' % url)
871 org_cursor = data.pop('cursor', None)
872 org_total = len(data.get('items') or [])
873 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
874 if not org_cursor or not org_total:
875 # This is not an iterable resource.
876 return data, lambda: []
877
878 def yielder():
879 cursor = org_cursor
880 total = org_total
881 # Some items support cursors. Try to get automatically if cursors are needed
882 # by looking at the 'cursor' items.
883 while cursor and (not limit or total < limit):
884 merge_char = '&' if '?' in base_url else '?'
885 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
886 if limit:
887 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
888 new = net.url_read_json(url)
889 if new is None:
890 raise Failure('Failed to access %s' % url)
891 cursor = new.get('cursor')
892 new_items = new.get('items')
893 nb_items = len(new_items or [])
894 total += nb_items
895 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
896 yield new_items
897
898 return data, yielder
899
900
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500901### Commands.
902
903
904def abort_task(_swarming, _manifest):
905 """Given a task manifest that was triggered, aborts its execution."""
906 # TODO(vadimsh): No supported by the server yet.
907
908
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800910 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500911 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500912 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500913 dest='dimensions', metavar='FOO bar',
914 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500915 parser.add_option_group(parser.filter_group)
916
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400917
maruelaf6b06c2017-06-08 06:26:53 -0700918def process_filter_options(parser, options):
919 for key, value in options.dimensions:
920 if ':' in key:
921 parser.error('--dimension key cannot contain ":"')
922 if key.strip() != key:
923 parser.error('--dimension key has whitespace')
924 if not key:
925 parser.error('--dimension key is empty')
926
927 if value.strip() != value:
928 parser.error('--dimension value has whitespace')
929 if not value:
930 parser.error('--dimension value is empty')
931 options.dimensions.sort()
932
933
Vadim Shtayurab450c602014-05-12 19:23:25 -0700934def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400935 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700936 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700937 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700938 help='Number of shards to trigger and collect.')
939 parser.add_option_group(parser.sharding_group)
940
941
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400942def add_trigger_options(parser):
943 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500944 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400945 add_filter_options(parser)
946
maruel681d6802017-01-17 16:56:03 -0800947 group = optparse.OptionGroup(parser, 'Task properties')
948 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700949 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500950 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800951 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500952 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700953 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800954 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800955 '--env-prefix', default=[], action='append', nargs=2,
956 metavar='VAR local/path',
957 help='Prepend task-relative `local/path` to the task\'s VAR environment '
958 'variable using os-appropriate pathsep character. Can be specified '
959 'multiple times for the same VAR to add multiple paths.')
960 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400961 '--idempotent', action='store_true', default=False,
962 help='When set, the server will actively try to find a previous task '
963 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800964 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700965 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700966 help='The optional path to a file containing the secret_bytes to use with'
967 'this task.')
maruel681d6802017-01-17 16:56:03 -0800968 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700969 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400970 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700972 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400973 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800974 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500975 '--raw-cmd', action='store_true', default=False,
976 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700977 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800978 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500979 '--relative-cwd',
980 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
981 'requires --raw-cmd')
982 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700983 '--cipd-package', action='append', default=[], metavar='PKG',
984 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700985 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800986 group.add_option(
987 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700988 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800989 help='"<name> <relpath>" items to keep a persistent bot managed cache')
990 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700991 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700992 help='Email of a service account to run the task as, or literal "bot" '
993 'string to indicate that the task should use the same account the '
994 'bot itself is using to authenticate to Swarming. Don\'t use task '
995 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800996 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700997 '-o', '--output', action='append', default=[], metavar='PATH',
998 help='A list of files to return in addition to those written to '
999 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1000 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -08001001 parser.add_option_group(group)
1002
1003 group = optparse.OptionGroup(parser, 'Task request')
1004 group.add_option(
1005 '--priority', type='int', default=100,
1006 help='The lower value, the more important the task is')
1007 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001008 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001009 help='Display name of the task. Defaults to '
1010 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1011 'isolated file is provided, if a hash is provided, it defaults to '
1012 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1013 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001014 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001015 help='Tags to assign to the task.')
1016 group.add_option(
1017 '--user', default='',
1018 help='User associated with the task. Defaults to authenticated user on '
1019 'the server.')
1020 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001021 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001022 help='Seconds to allow the task to be pending for a bot to run before '
1023 'this task request expires.')
1024 group.add_option(
1025 '--deadline', type='int', dest='expiration',
1026 help=optparse.SUPPRESS_HELP)
1027 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001028
1029
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001030def process_trigger_options(parser, options, args):
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001031 """Processes trigger options and does preparatory steps."""
maruelaf6b06c2017-06-08 06:26:53 -07001032 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001033 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001034 if args and args[0] == '--':
1035 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001036
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001037 if not options.dimensions:
1038 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001039 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1040 parser.error('--tags must be in the format key:value')
1041 if options.raw_cmd and not args:
1042 parser.error(
1043 'Arguments with --raw-cmd should be passed after -- as command '
1044 'delimiter.')
1045 if options.isolate_server and not options.namespace:
1046 parser.error(
1047 '--namespace must be a valid value when --isolate-server is used')
1048 if not options.isolated and not options.raw_cmd:
1049 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1050
1051 # Isolated
1052 # --isolated is required only if --raw-cmd wasn't provided.
1053 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1054 # preferred server.
1055 isolateserver.process_isolate_server_options(
1056 parser, options, False, not options.raw_cmd)
1057 inputs_ref = None
1058 if options.isolate_server:
1059 inputs_ref = FilesRef(
1060 isolated=options.isolated,
1061 isolatedserver=options.isolate_server,
1062 namespace=options.namespace)
1063
1064 # Command
1065 command = None
1066 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001067 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001068 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001069 if options.relative_cwd:
1070 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1071 if not a.startswith(os.getcwd()):
1072 parser.error(
1073 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001074 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001075 if options.relative_cwd:
1076 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001077 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001078
maruel0a25f6c2017-05-10 10:43:23 -07001079 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001080 cipd_packages = []
1081 for p in options.cipd_package:
1082 split = p.split(':', 2)
1083 if len(split) != 3:
1084 parser.error('CIPD packages must take the form: path:package:version')
1085 cipd_packages.append(CipdPackage(
1086 package_name=split[1],
1087 path=split[0],
1088 version=split[2]))
1089 cipd_input = None
1090 if cipd_packages:
1091 cipd_input = CipdInput(
1092 client_package=None,
1093 packages=cipd_packages,
1094 server=None)
1095
maruel0a25f6c2017-05-10 10:43:23 -07001096 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001097 secret_bytes = None
1098 if options.secret_bytes_path:
1099 with open(options.secret_bytes_path, 'r') as f:
1100 secret_bytes = f.read().encode('base64')
1101
maruel0a25f6c2017-05-10 10:43:23 -07001102 # Named caches
maruel681d6802017-01-17 16:56:03 -08001103 caches = [
1104 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1105 for i in options.named_cache
1106 ]
maruel0a25f6c2017-05-10 10:43:23 -07001107
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001108 env_prefixes = {}
1109 for k, v in options.env_prefix:
1110 env_prefixes.setdefault(k, []).append(v)
1111
maruel77f720b2015-09-15 12:35:22 -07001112 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001113 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001114 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001115 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001116 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001117 dimensions=options.dimensions,
1118 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001119 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001120 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001121 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001122 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001123 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001124 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001125 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001126 outputs=options.output,
1127 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001128
maruel77f720b2015-09-15 12:35:22 -07001129 return NewTaskRequest(
1130 expiration_secs=options.expiration,
maruel0a25f6c2017-05-10 10:43:23 -07001131 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001132 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001133 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001134 properties=properties,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001135 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001136 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001137 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001138
1139
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001140class TaskOutputStdoutOption(optparse.Option):
1141 """Where to output the each task's console output (stderr/stdout).
1142
1143 The output will be;
1144 none - not be downloaded.
1145 json - stored in summary.json file *only*.
1146 console - shown on stdout *only*.
1147 all - stored in summary.json and shown on stdout.
1148 """
1149
1150 choices = ['all', 'json', 'console', 'none']
1151
1152 def __init__(self, *args, **kw):
1153 optparse.Option.__init__(
1154 self,
1155 *args,
1156 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001157 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001158 help=re.sub('\s\s*', ' ', self.__doc__),
1159 **kw)
1160
1161 def convert_value(self, opt, value):
1162 if value not in self.choices:
1163 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1164 self.get_opt_string(), self.choices, value))
1165 stdout_to = []
1166 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001167 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001168 elif value != 'none':
1169 stdout_to = [value]
1170 return stdout_to
1171
1172
maruel@chromium.org0437a732013-08-27 16:05:52 +00001173def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001174 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001175 '-t', '--timeout', type='float',
1176 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1177 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001178 parser.group_logging.add_option(
1179 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001180 parser.group_logging.add_option(
1181 '--print-status-updates', action='store_true',
1182 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001183 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001184 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001185 '--task-summary-json',
1186 metavar='FILE',
1187 help='Dump a summary of task results to this file as json. It contains '
1188 'only shards statuses as know to server directly. Any output files '
1189 'emitted by the task can be collected by using --task-output-dir')
1190 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001191 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001192 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001193 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001194 'directory contains per-shard directory with output files produced '
1195 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001196 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001197 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001198 parser.task_output_group.add_option(
1199 '--perf', action='store_true', default=False,
1200 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001201 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001202
1203
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001204@subcommand.usage('bots...')
1205def CMDbot_delete(parser, args):
1206 """Forcibly deletes bots from the Swarming server."""
1207 parser.add_option(
1208 '-f', '--force', action='store_true',
1209 help='Do not prompt for confirmation')
1210 options, args = parser.parse_args(args)
1211 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001212 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001213
1214 bots = sorted(args)
1215 if not options.force:
1216 print('Delete the following bots?')
1217 for bot in bots:
1218 print(' %s' % bot)
1219 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1220 print('Goodbye.')
1221 return 1
1222
1223 result = 0
1224 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001225 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001226 if net.url_read_json(url, data={}, method='POST') is None:
1227 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001228 result = 1
1229 return result
1230
1231
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001232def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001233 """Returns information about the bots connected to the Swarming server."""
1234 add_filter_options(parser)
1235 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001236 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001237 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001238 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001239 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001240 help='Keep both dead and alive bots')
1241 parser.filter_group.add_option(
1242 '--busy', action='store_true', help='Keep only busy bots')
1243 parser.filter_group.add_option(
1244 '--idle', action='store_true', help='Keep only idle bots')
1245 parser.filter_group.add_option(
1246 '--mp', action='store_true',
1247 help='Keep only Machine Provider managed bots')
1248 parser.filter_group.add_option(
1249 '--non-mp', action='store_true',
1250 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001251 parser.filter_group.add_option(
1252 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001253 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001254 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001255 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001256
1257 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001258 parser.error('Use only one of --keep-dead or --dead-only')
1259 if options.busy and options.idle:
1260 parser.error('Use only one of --busy or --idle')
1261 if options.mp and options.non_mp:
1262 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001263
maruelaf6b06c2017-06-08 06:26:53 -07001264 url = options.swarming + '/api/swarming/v1/bots/list?'
1265 values = []
1266 if options.dead_only:
1267 values.append(('is_dead', 'TRUE'))
1268 elif options.keep_dead:
1269 values.append(('is_dead', 'NONE'))
1270 else:
1271 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001272
maruelaf6b06c2017-06-08 06:26:53 -07001273 if options.busy:
1274 values.append(('is_busy', 'TRUE'))
1275 elif options.idle:
1276 values.append(('is_busy', 'FALSE'))
1277 else:
1278 values.append(('is_busy', 'NONE'))
1279
1280 if options.mp:
1281 values.append(('is_mp', 'TRUE'))
1282 elif options.non_mp:
1283 values.append(('is_mp', 'FALSE'))
1284 else:
1285 values.append(('is_mp', 'NONE'))
1286
1287 for key, value in options.dimensions:
1288 values.append(('dimensions', '%s:%s' % (key, value)))
1289 url += urllib.urlencode(values)
1290 try:
1291 data, yielder = get_yielder(url, 0)
1292 bots = data.get('items') or []
1293 for items in yielder():
1294 if items:
1295 bots.extend(items)
1296 except Failure as e:
1297 sys.stderr.write('\n%s\n' % e)
1298 return 1
maruel77f720b2015-09-15 12:35:22 -07001299 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001300 print bot['bot_id']
1301 if not options.bare:
1302 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1303 print ' %s' % json.dumps(dimensions, sort_keys=True)
1304 if bot.get('task_id'):
1305 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001306 return 0
1307
1308
maruelfd0a90c2016-06-10 11:51:10 -07001309@subcommand.usage('task_id')
1310def CMDcancel(parser, args):
1311 """Cancels a task."""
1312 options, args = parser.parse_args(args)
1313 if not args:
1314 parser.error('Please specify the task to cancel')
1315 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001316 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001317 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1318 print('Deleting %s failed. Probably already gone' % task_id)
1319 return 1
1320 return 0
1321
1322
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001323@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001324def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001325 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001326
1327 The result can be in multiple part if the execution was sharded. It can
1328 potentially have retries.
1329 """
1330 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001331 parser.add_option(
1332 '-j', '--json',
1333 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001334 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001335 if not args and not options.json:
1336 parser.error('Must specify at least one task id or --json.')
1337 if args and options.json:
1338 parser.error('Only use one of task id or --json.')
1339
1340 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001341 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001342 try:
maruel1ceb3872015-10-14 06:10:44 -07001343 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001344 data = json.load(f)
1345 except (IOError, ValueError):
1346 parser.error('Failed to open %s' % options.json)
1347 try:
1348 tasks = sorted(
1349 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1350 args = [t['task_id'] for t in tasks]
1351 except (KeyError, TypeError):
1352 parser.error('Failed to process %s' % options.json)
1353 if options.timeout is None:
1354 options.timeout = (
1355 data['request']['properties']['execution_timeout_secs'] +
1356 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001357 else:
1358 valid = frozenset('0123456789abcdef')
1359 if any(not valid.issuperset(task_id) for task_id in args):
1360 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001361
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001362 try:
1363 return collect(
1364 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001365 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001366 options.timeout,
1367 options.decorate,
1368 options.print_status_updates,
1369 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001370 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001371 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001372 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001373 except Failure:
1374 on_error.report(None)
1375 return 1
1376
1377
maruel77f720b2015-09-15 12:35:22 -07001378@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001379def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001380 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1381 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001382
1383 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001384 Raw task request and results:
1385 swarming.py query -S server-url.com task/123456/request
1386 swarming.py query -S server-url.com task/123456/result
1387
maruel77f720b2015-09-15 12:35:22 -07001388 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001389 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001390
maruelaf6b06c2017-06-08 06:26:53 -07001391 Listing last 10 tasks on a specific bot named 'bot1':
1392 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001393
maruelaf6b06c2017-06-08 06:26:53 -07001394 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001395 quoting is important!:
1396 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001397 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001398 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001399 parser.add_option(
1400 '-L', '--limit', type='int', default=200,
1401 help='Limit to enforce on limitless items (like number of tasks); '
1402 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001403 parser.add_option(
1404 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001405 parser.add_option(
1406 '--progress', action='store_true',
1407 help='Prints a dot at each request to show progress')
1408 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001409 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001410 parser.error(
1411 'Must specify only method name and optionally query args properly '
1412 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001413 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001414 try:
1415 data, yielder = get_yielder(base_url, options.limit)
1416 for items in yielder():
1417 if items:
1418 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001419 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001420 sys.stderr.write('.')
1421 sys.stderr.flush()
1422 except Failure as e:
1423 sys.stderr.write('\n%s\n' % e)
1424 return 1
maruel77f720b2015-09-15 12:35:22 -07001425 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001426 sys.stderr.write('\n')
1427 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001428 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001429 options.json = unicode(os.path.abspath(options.json))
1430 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001431 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001432 try:
maruel77f720b2015-09-15 12:35:22 -07001433 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001434 sys.stdout.write('\n')
1435 except IOError:
1436 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001437 return 0
1438
1439
maruel77f720b2015-09-15 12:35:22 -07001440def CMDquery_list(parser, args):
1441 """Returns list of all the Swarming APIs that can be used with command
1442 'query'.
1443 """
1444 parser.add_option(
1445 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1446 options, args = parser.parse_args(args)
1447 if args:
1448 parser.error('No argument allowed.')
1449
1450 try:
1451 apis = endpoints_api_discovery_apis(options.swarming)
1452 except APIError as e:
1453 parser.error(str(e))
1454 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001455 options.json = unicode(os.path.abspath(options.json))
1456 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001457 json.dump(apis, f)
1458 else:
1459 help_url = (
1460 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1461 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001462 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1463 if i:
1464 print('')
maruel77f720b2015-09-15 12:35:22 -07001465 print api_id
maruel11e31af2017-02-15 07:30:50 -08001466 print ' ' + api['description'].strip()
1467 if 'resources' in api:
1468 # Old.
1469 for j, (resource_name, resource) in enumerate(
1470 sorted(api['resources'].iteritems())):
1471 if j:
1472 print('')
1473 for method_name, method in sorted(resource['methods'].iteritems()):
1474 # Only list the GET ones.
1475 if method['httpMethod'] != 'GET':
1476 continue
1477 print '- %s.%s: %s' % (
1478 resource_name, method_name, method['path'])
1479 print('\n'.join(
1480 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1481 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1482 else:
1483 # New.
1484 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001485 # Only list the GET ones.
1486 if method['httpMethod'] != 'GET':
1487 continue
maruel11e31af2017-02-15 07:30:50 -08001488 print '- %s: %s' % (method['id'], method['path'])
1489 print('\n'.join(
1490 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001491 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1492 return 0
1493
1494
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001495@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001496def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001497 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001498
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001499 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001500 """
1501 add_trigger_options(parser)
1502 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001503 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001504 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001505 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001506 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001507 tasks = trigger_task_shards(
1508 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001509 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001510 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001511 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001512 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001513 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001514 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001515 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001516 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001517 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001518 task_ids = [
1519 t['task_id']
1520 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1521 ]
maruel71c61c82016-02-22 06:52:05 -08001522 if options.timeout is None:
1523 options.timeout = (
1524 task_request.properties.execution_timeout_secs +
1525 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001526 try:
1527 return collect(
1528 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001529 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001530 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001531 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001532 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001533 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001534 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001535 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001536 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001537 except Failure:
1538 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001539 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001540
1541
maruel18122c62015-10-23 06:31:23 -07001542@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001543def CMDreproduce(parser, args):
1544 """Runs a task locally that was triggered on the server.
1545
1546 This running locally the same commands that have been run on the bot. The data
1547 downloaded will be in a subdirectory named 'work' of the current working
1548 directory.
maruel18122c62015-10-23 06:31:23 -07001549
1550 You can pass further additional arguments to the target command by passing
1551 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001552 """
maruelc070e672016-02-22 17:32:57 -08001553 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001554 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001555 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001556 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001557 extra_args = []
1558 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001559 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001560 if len(args) > 1:
1561 if args[1] == '--':
1562 if len(args) > 2:
1563 extra_args = args[2:]
1564 else:
1565 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001566
maruel380e3262016-08-31 16:10:06 -07001567 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001568 request = net.url_read_json(url)
1569 if not request:
1570 print >> sys.stderr, 'Failed to retrieve request data for the task'
1571 return 1
1572
maruel12e30012015-10-09 11:55:35 -07001573 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001574 if fs.isdir(workdir):
1575 parser.error('Please delete the directory \'work\' first')
1576 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001577 cachedir = unicode(os.path.abspath('cipd_cache'))
1578 if not fs.exists(cachedir):
1579 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001580
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001581 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001582 env = os.environ.copy()
1583 env['SWARMING_BOT_ID'] = 'reproduce'
1584 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001585 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001586 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001587 for i in properties['env']:
1588 key = i['key'].encode('utf-8')
1589 if not i['value']:
1590 env.pop(key, None)
1591 else:
1592 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001593
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001594 if properties.get('env_prefixes'):
1595 env_prefixes = properties['env']
1596 logging.info('env_prefixes: %r', env_prefixes)
1597 for key, paths in env_prefixes.iteritems():
1598 paths = [os.path.normpath(os.path.join(workdir, p)) for p in paths]
1599 cur = env.get(key)
1600 if cur:
1601 paths.append(cur)
1602 env[key] = os.path.pathsep.join(paths)
1603
iannucci31ab9192017-05-02 19:11:56 -07001604 command = []
nodir152cba62016-05-12 16:08:56 -07001605 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001606 # Create the tree.
1607 with isolateserver.get_storage(
1608 properties['inputs_ref']['isolatedserver'],
1609 properties['inputs_ref']['namespace']) as storage:
1610 bundle = isolateserver.fetch_isolated(
1611 properties['inputs_ref']['isolated'],
1612 storage,
1613 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001614 workdir,
1615 False)
maruel29ab2fd2015-10-16 11:44:01 -07001616 command = bundle.command
1617 if bundle.relative_cwd:
1618 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001619 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001620
1621 if properties.get('command'):
1622 command.extend(properties['command'])
1623
1624 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1625 new_command = tools.fix_python_path(command)
1626 new_command = run_isolated.process_command(
1627 new_command, options.output_dir, None)
1628 if not options.output_dir and new_command != command:
1629 parser.error('The task has outputs, you must use --output-dir')
1630 command = new_command
1631 file_path.ensure_command_has_abs_path(command, workdir)
1632
1633 if properties.get('cipd_input'):
1634 ci = properties['cipd_input']
1635 cp = ci['client_package']
1636 client_manager = cipd.get_client(
1637 ci['server'], cp['package_name'], cp['version'], cachedir)
1638
1639 with client_manager as client:
1640 by_path = collections.defaultdict(list)
1641 for pkg in ci['packages']:
1642 path = pkg['path']
1643 # cipd deals with 'root' as ''
1644 if path == '.':
1645 path = ''
1646 by_path[path].append((pkg['package_name'], pkg['version']))
1647 client.ensure(workdir, by_path, cache_dir=cachedir)
1648
maruel77f720b2015-09-15 12:35:22 -07001649 try:
maruel18122c62015-10-23 06:31:23 -07001650 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001651 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001652 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001653 print >> sys.stderr, str(e)
1654 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001655
1656
maruel0eb1d1b2015-10-02 14:48:21 -07001657@subcommand.usage('bot_id')
1658def CMDterminate(parser, args):
1659 """Tells a bot to gracefully shut itself down as soon as it can.
1660
1661 This is done by completing whatever current task there is then exiting the bot
1662 process.
1663 """
1664 parser.add_option(
1665 '--wait', action='store_true', help='Wait for the bot to terminate')
1666 options, args = parser.parse_args(args)
1667 if len(args) != 1:
1668 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001669 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001670 request = net.url_read_json(url, data={})
1671 if not request:
1672 print >> sys.stderr, 'Failed to ask for termination'
1673 return 1
1674 if options.wait:
1675 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001676 options.swarming,
1677 [request['task_id']],
1678 0.,
1679 False,
1680 False,
1681 None,
1682 None,
1683 [],
maruel9531ce02016-04-13 06:11:23 -07001684 False)
maruelbfc5f872017-06-10 16:43:17 -07001685 else:
1686 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001687 return 0
1688
1689
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001690@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001691def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001692 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001693
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001694 Passes all extra arguments provided after '--' as additional command line
1695 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001696 """
1697 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001698 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001699 parser.add_option(
1700 '--dump-json',
1701 metavar='FILE',
1702 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001703 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001704 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001705 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001706 tasks = trigger_task_shards(
1707 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001708 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001709 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001710 tasks_sorted = sorted(
1711 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001712 if options.dump_json:
1713 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001714 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001715 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001716 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001717 }
maruel46b015f2015-10-13 18:40:35 -07001718 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001719 print('To collect results, use:')
1720 print(' swarming.py collect -S %s --json %s' %
1721 (options.swarming, options.dump_json))
1722 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001723 print('To collect results, use:')
1724 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001725 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1726 print('Or visit:')
1727 for t in tasks_sorted:
1728 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001729 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001730 except Failure:
1731 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001732 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001733
1734
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001735class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001736 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001737 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001738 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001739 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001740 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001741 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001742 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001743 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001744 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001745 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001746
1747 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001748 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001749 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001750 auth.process_auth_options(self, options)
1751 user = self._process_swarming(options)
1752 if hasattr(options, 'user') and not options.user:
1753 options.user = user
1754 return options, args
1755
1756 def _process_swarming(self, options):
1757 """Processes the --swarming option and aborts if not specified.
1758
1759 Returns the identity as determined by the server.
1760 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001761 if not options.swarming:
1762 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001763 try:
1764 options.swarming = net.fix_url(options.swarming)
1765 except ValueError as e:
1766 self.error('--swarming %s' % e)
1767 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001768 try:
1769 user = auth.ensure_logged_in(options.swarming)
1770 except ValueError as e:
1771 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001772 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001773
1774
1775def main(args):
1776 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001777 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001778
1779
1780if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001781 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001782 fix_encoding.fix_encoding()
1783 tools.disable_buffering()
1784 colorama.init()
1785 sys.exit(main(sys.argv[1:]))