blob: 6762912ac440a259d7fbd3b03edbb829d32ed1ad [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005
6"""Client tool to trigger tasks or retrieve results from a Swarming server."""
7
Robert Iannuccibe66ce72017-11-22 12:56:50 -08008__version__ = '0.10.0'
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' Ansell4c3f1682017-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
maruela9fe2cb2017-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:
maruela9fe2cb2017-05-10 10:43:23 -070056 task_name = u'%s/%s' % (
maruel4e901792017-05-09 12:07:02 -070057 options.user,
maruel0165e822017-06-08 06:26:53 -070058 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruela9fe2cb2017-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 Iannuccibe66ce72017-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 Ruelefdc5282014-12-12 19:31:00 -0500114 'dimensions',
115 'env',
Robert Iannuccibe66ce72017-11-22 12:56:50 -0800116 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700117 'execution_timeout_secs',
118 'extra_args',
119 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500120 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700121 'inputs_ref',
122 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700123 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700124 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700125 ])
126
127
128# See ../appengine/swarming/swarming_rpcs.py.
129NewTaskRequest = collections.namedtuple(
130 'NewTaskRequest',
131 [
132 'expiration_secs',
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',
maruel77f720b2015-09-15 12:35:22 -0700136 'properties',
Vadim Shtayura9aef3f12017-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."""
145 out = dict(value._asdict())
146 for k, v in out.iteritems():
147 if hasattr(v, '_asdict'):
148 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700149 elif isinstance(v, (list, tuple)):
150 l = []
151 for elem in v:
152 if hasattr(elem, '_asdict'):
153 l.append(namedtuple_to_dict(elem))
154 else:
155 l.append(elem)
156 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700157 return out
158
159
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700160def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800161 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700162
163 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 """
maruel77f720b2015-09-15 12:35:22 -0700165 out = namedtuple_to_dict(task_request)
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700166 # Don't send 'service_account' if it is None to avoid confusing older
167 # version of the server that doesn't know about 'service_account' and don't
168 # use it at all.
169 if not out['service_account']:
170 out.pop('service_account')
maruel77f720b2015-09-15 12:35:22 -0700171 out['properties']['dimensions'] = [
172 {'key': k, 'value': v}
maruel0165e822017-06-08 06:26:53 -0700173 for k, v in out['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700174 ]
maruel77f720b2015-09-15 12:35:22 -0700175 out['properties']['env'] = [
176 {'key': k, 'value': v}
177 for k, v in out['properties']['env'].iteritems()
178 ]
179 out['properties']['env'].sort(key=lambda x: x['key'])
180 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500181
182
maruel77f720b2015-09-15 12:35:22 -0700183def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500184 """Triggers a request on the Swarming server and returns the json data.
185
186 It's the low-level function.
187
188 Returns:
189 {
190 'request': {
191 'created_ts': u'2010-01-02 03:04:05',
192 'name': ..
193 },
194 'task_id': '12300',
195 }
196 """
197 logging.info('Triggering: %s', raw_request['name'])
198
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700200 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500201 if not result:
202 on_error.report('Failed to trigger task %s' % raw_request['name'])
203 return None
maruele557bce2015-11-17 09:01:27 -0800204 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800205 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800206 msg = 'Failed to trigger task %s' % raw_request['name']
207 if result['error'].get('errors'):
208 for err in result['error']['errors']:
209 if err.get('message'):
210 msg += '\nMessage: %s' % err['message']
211 if err.get('debugInfo'):
212 msg += '\nDebug info:\n%s' % err['debugInfo']
213 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800214 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800215
216 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800217 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500218 return result
219
220
221def setup_googletest(env, shards, index):
222 """Sets googletest specific environment variables."""
223 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700224 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
225 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
226 env = env[:]
227 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
228 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500229 return env
230
231
232def trigger_task_shards(swarming, task_request, shards):
233 """Triggers one or many subtasks of a sharded task.
234
235 Returns:
236 Dict with task details, returned to caller as part of --dump-json output.
237 None in case of failure.
238 """
239 def convert(index):
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700240 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500241 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700242 req['properties']['env'] = setup_googletest(
243 req['properties']['env'], shards, index)
244 req['name'] += ':%s:%s' % (index, shards)
245 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500246
247 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 tasks = {}
249 priority_warning = False
250 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700251 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500252 if not task:
253 break
254 logging.info('Request result: %s', task)
255 if (not priority_warning and
Marc-Antoine Ruel49ea2182017-08-17 10:07:49 -0400256 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500257 priority_warning = True
258 print >> sys.stderr, (
259 'Priority was reset to %s' % task['request']['priority'])
260 tasks[request['name']] = {
261 'shard_index': index,
262 'task_id': task['task_id'],
263 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
264 }
265
266 # Some shards weren't triggered. Abort everything.
267 if len(tasks) != len(requests):
268 if tasks:
269 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
270 len(tasks), len(requests))
271 for task_dict in tasks.itervalues():
272 abort_task(swarming, task_dict['task_id'])
273 return None
274
275 return tasks
276
277
278### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000279
280
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700281# How often to print status updates to stdout in 'collect'.
282STATUS_UPDATE_INTERVAL = 15 * 60.
283
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400284
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400285class State(object):
286 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000287
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400288 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
289 values are part of the API so if they change, the API changed.
290
291 It's in fact an enum. Values should be in decreasing order of importance.
292 """
293 RUNNING = 0x10
294 PENDING = 0x20
295 EXPIRED = 0x30
296 TIMED_OUT = 0x40
297 BOT_DIED = 0x50
298 CANCELED = 0x60
299 COMPLETED = 0x70
300
maruel77f720b2015-09-15 12:35:22 -0700301 STATES = (
302 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
303 'COMPLETED')
304 STATES_RUNNING = ('RUNNING', 'PENDING')
305 STATES_NOT_RUNNING = (
306 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
307 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
308 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400309
310 _NAMES = {
311 RUNNING: 'Running',
312 PENDING: 'Pending',
313 EXPIRED: 'Expired',
314 TIMED_OUT: 'Execution timed out',
315 BOT_DIED: 'Bot died',
316 CANCELED: 'User canceled',
317 COMPLETED: 'Completed',
318 }
319
maruel77f720b2015-09-15 12:35:22 -0700320 _ENUMS = {
321 'RUNNING': RUNNING,
322 'PENDING': PENDING,
323 'EXPIRED': EXPIRED,
324 'TIMED_OUT': TIMED_OUT,
325 'BOT_DIED': BOT_DIED,
326 'CANCELED': CANCELED,
327 'COMPLETED': COMPLETED,
328 }
329
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400330 @classmethod
331 def to_string(cls, state):
332 """Returns a user-readable string representing a State."""
333 if state not in cls._NAMES:
334 raise ValueError('Invalid state %s' % state)
335 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000336
maruel77f720b2015-09-15 12:35:22 -0700337 @classmethod
338 def from_enum(cls, state):
339 """Returns int value based on the string."""
340 if state not in cls._ENUMS:
341 raise ValueError('Invalid state %s' % state)
342 return cls._ENUMS[state]
343
maruel@chromium.org0437a732013-08-27 16:05:52 +0000344
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700345class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700346 """Assembles task execution summary (for --task-summary-json output).
347
348 Optionally fetches task outputs from isolate server to local disk (used when
349 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700350
351 This object is shared among multiple threads running 'retrieve_results'
352 function, in particular they call 'process_shard_result' method in parallel.
353 """
354
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000355 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700356 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
357
358 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700359 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360 shard_count: expected number of task shards.
361 """
maruel12e30012015-10-09 11:55:35 -0700362 self.task_output_dir = (
363 unicode(os.path.abspath(task_output_dir))
364 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000365 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700366 self.shard_count = shard_count
367
368 self._lock = threading.Lock()
369 self._per_shard_results = {}
370 self._storage = None
371
nodire5028a92016-04-29 14:38:21 -0700372 if self.task_output_dir:
373 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374
Vadim Shtayurab450c602014-05-12 19:23:25 -0700375 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376 """Stores results of a single task shard, fetches output files if necessary.
377
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400378 Modifies |result| in place.
379
maruel77f720b2015-09-15 12:35:22 -0700380 shard_index is 0-based.
381
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 Called concurrently from multiple threads.
383 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700385 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 if shard_index < 0 or shard_index >= self.shard_count:
387 logging.warning(
388 'Shard index %d is outside of expected range: [0; %d]',
389 shard_index, self.shard_count - 1)
390 return
391
maruel77f720b2015-09-15 12:35:22 -0700392 if result.get('outputs_ref'):
393 ref = result['outputs_ref']
394 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
395 ref['isolatedserver'],
396 urllib.urlencode(
397 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400398
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399 # Store result dict of that shard, ignore results we've already seen.
400 with self._lock:
401 if shard_index in self._per_shard_results:
402 logging.warning('Ignoring duplicate shard index %d', shard_index)
403 return
404 self._per_shard_results[shard_index] = result
405
406 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700407 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400408 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700409 result['outputs_ref']['isolatedserver'],
410 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400411 if storage:
412 # Output files are supposed to be small and they are not reused across
413 # tasks. So use MemoryCache for them instead of on-disk cache. Make
414 # files writable, so that calling script can delete them.
415 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700416 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400417 storage,
418 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700419 os.path.join(self.task_output_dir, str(shard_index)),
420 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700421
422 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700423 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424 with self._lock:
425 # Write an array of shard results with None for missing shards.
426 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427 'shards': [
428 self._per_shard_results.get(i) for i in xrange(self.shard_count)
429 ],
430 }
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000431
432 # Don't store stdout in the summary if not requested too.
433 if "json" not in self.task_output_stdout:
434 for shard_json in summary['shards']:
435 if not shard_json:
436 continue
437 if "output" in shard_json:
438 del shard_json["output"]
439 if "outputs" in shard_json:
440 del shard_json["outputs"]
441
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 # Write summary.json to task_output_dir as well.
443 if self.task_output_dir:
444 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700445 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700446 summary,
447 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 if self._storage:
449 self._storage.close()
450 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700451 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452
453 def _get_storage(self, isolate_server, namespace):
454 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700455 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 with self._lock:
457 if not self._storage:
458 self._storage = isolateserver.get_storage(isolate_server, namespace)
459 else:
460 # Shards must all use exact same isolate server and namespace.
461 if self._storage.location != isolate_server:
462 logging.error(
463 'Task shards are using multiple isolate servers: %s and %s',
464 self._storage.location, isolate_server)
465 return None
466 if self._storage.namespace != namespace:
467 logging.error(
468 'Task shards are using multiple namespaces: %s and %s',
469 self._storage.namespace, namespace)
470 return None
471 return self._storage
472
473
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500474def now():
475 """Exists so it can be mocked easily."""
476 return time.time()
477
478
maruel77f720b2015-09-15 12:35:22 -0700479def parse_time(value):
480 """Converts serialized time from the API to datetime.datetime."""
481 # When microseconds are 0, the '.123456' suffix is elided. This means the
482 # serialized format is not consistent, which confuses the hell out of python.
483 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
484 try:
485 return datetime.datetime.strptime(value, fmt)
486 except ValueError:
487 pass
488 raise ValueError('Failed to parse %s' % value)
489
490
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700491def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700492 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000493 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400494 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700495
Vadim Shtayurab450c602014-05-12 19:23:25 -0700496 Returns:
497 <result dict> on success.
498 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700499 """
maruel71c61c82016-02-22 06:52:05 -0800500 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700501 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700502 if include_perf:
503 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700504 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700505 started = now()
506 deadline = started + timeout if timeout else None
507 attempt = 0
508
509 while not should_stop.is_set():
510 attempt += 1
511
512 # Waiting for too long -> give up.
513 current_time = now()
514 if deadline and current_time >= deadline:
515 logging.error('retrieve_results(%s) timed out on attempt %d',
516 base_url, attempt)
517 return None
518
519 # Do not spin too fast. Spin faster at the beginning though.
520 # Start with 1 sec delay and for each 30 sec of waiting add another second
521 # of delay, until hitting 15 sec ceiling.
522 if attempt > 1:
523 max_delay = min(15, 1 + (current_time - started) / 30.0)
524 delay = min(max_delay, deadline - current_time) if deadline else max_delay
525 if delay > 0:
526 logging.debug('Waiting %.1f sec before retrying', delay)
527 should_stop.wait(delay)
528 if should_stop.is_set():
529 return None
530
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 # Disable internal retries in net.url_read_json, since we are doing retries
532 # ourselves.
533 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700534 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
535 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400536 result = net.url_read_json(result_url, retry_50x=False)
537 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400538 continue
maruel77f720b2015-09-15 12:35:22 -0700539
maruelbf53e042015-12-01 15:00:51 -0800540 if result.get('error'):
541 # An error occurred.
542 if result['error'].get('errors'):
543 for err in result['error']['errors']:
544 logging.warning(
545 'Error while reading task: %s; %s',
546 err.get('message'), err.get('debugInfo'))
547 elif result['error'].get('message'):
548 logging.warning(
549 'Error while reading task: %s', result['error']['message'])
550 continue
551
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400552 if result['state'] in State.STATES_NOT_RUNNING:
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000553 if fetch_stdout:
554 out = net.url_read_json(output_url)
Vadim Shtayura96eff0d2017-11-03 15:32:51 -0700555 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700556 # Record the result, try to fetch attached output files (if any).
557 if output_collector:
558 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700559 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700560 if result.get('internal_failure'):
561 logging.error('Internal error!')
562 elif result['state'] == 'BOT_DIED':
563 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700564 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000565
566
maruel77f720b2015-09-15 12:35:22 -0700567def convert_to_old_format(result):
568 """Converts the task result data from Endpoints API format to old API format
569 for compatibility.
570
571 This goes into the file generated as --task-summary-json.
572 """
573 # Sets default.
574 result.setdefault('abandoned_ts', None)
575 result.setdefault('bot_id', None)
576 result.setdefault('bot_version', None)
577 result.setdefault('children_task_ids', [])
578 result.setdefault('completed_ts', None)
579 result.setdefault('cost_saved_usd', None)
580 result.setdefault('costs_usd', None)
581 result.setdefault('deduped_from', None)
582 result.setdefault('name', None)
583 result.setdefault('outputs_ref', None)
584 result.setdefault('properties_hash', None)
585 result.setdefault('server_versions', None)
586 result.setdefault('started_ts', None)
587 result.setdefault('tags', None)
588 result.setdefault('user', None)
589
590 # Convertion back to old API.
591 duration = result.pop('duration', None)
592 result['durations'] = [duration] if duration else []
593 exit_code = result.pop('exit_code', None)
594 result['exit_codes'] = [int(exit_code)] if exit_code else []
595 result['id'] = result.pop('task_id')
596 result['isolated_out'] = result.get('outputs_ref', None)
597 output = result.pop('output', None)
598 result['outputs'] = [output] if output else []
599 # properties_hash
600 # server_version
601 # Endpoints result 'state' as string. For compatibility with old code, convert
602 # to int.
603 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700604 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700605 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700606 if 'bot_dimensions' in result:
607 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700608 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700609 }
610 else:
611 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700612
613
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700614def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400615 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000616 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500617 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000618
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700619 Duplicate shards are ignored. Shards are yielded in order of completion.
620 Timed out shards are NOT yielded at all. Caller can compare number of yielded
621 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000622
623 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500624 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 +0000625 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500626
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700627 output_collector is an optional instance of TaskOutputCollector that will be
628 used to fetch files produced by a task from isolate server to the local disk.
629
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500630 Yields:
631 (index, result). In particular, 'result' is defined as the
632 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000633 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000634 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400635 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700636 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700637 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700638
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
640 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700641 # Adds a task to the thread pool to call 'retrieve_results' and return
642 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400643 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700644 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000645 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400646 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000647 task_id, timeout, should_stop, output_collector, include_perf,
648 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700649
650 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400651 for shard_index, task_id in enumerate(task_ids):
652 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653
654 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400655 shards_remaining = range(len(task_ids))
656 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700657 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700658 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700659 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700660 shard_index, result = results_channel.pull(
661 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700662 except threading_utils.TaskChannel.Timeout:
663 if print_status_updates:
664 print(
665 'Waiting for results from the following shards: %s' %
666 ', '.join(map(str, shards_remaining)))
667 sys.stdout.flush()
668 continue
669 except Exception:
670 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700671
672 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700673 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000674 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500675 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000676 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700677
Vadim Shtayurab450c602014-05-12 19:23:25 -0700678 # Yield back results to the caller.
679 assert shard_index in shards_remaining
680 shards_remaining.remove(shard_index)
681 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700682
maruel@chromium.org0437a732013-08-27 16:05:52 +0000683 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700684 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000685 should_stop.set()
686
687
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000688def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000689 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700690 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400691 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700692 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
693 ).total_seconds()
Marc-Antoine Ruel6bb4fb32017-11-03 14:34:49 -0400694 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
695 metadata.get('abandoned_ts')):
696 pending = '%.1fs' % (
697 parse_time(metadata['abandoned_ts']) -
698 parse_time(metadata['created_ts'])
699 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400700 else:
701 pending = 'N/A'
702
maruel77f720b2015-09-15 12:35:22 -0700703 if metadata.get('duration') is not None:
704 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400705 else:
706 duration = 'N/A'
707
maruel77f720b2015-09-15 12:35:22 -0700708 if metadata.get('exit_code') is not None:
709 # Integers are encoded as string to not loose precision.
710 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400711 else:
712 exit_code = 'N/A'
713
714 bot_id = metadata.get('bot_id') or 'N/A'
715
maruel77f720b2015-09-15 12:35:22 -0700716 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400717 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000718 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel6bb4fb32017-11-03 14:34:49 -0400719 if metadata.get('state') == 'CANCELED':
720 tag_footer2 = ' Pending: %s CANCELED' % pending
721 elif metadata.get('state') == 'EXPIRED':
722 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
723 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT'):
724 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
725 pending, duration, bot_id, exit_code, metadata['state'])
726 else:
727 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
728 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400729
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000730 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
731 dash_pad = '+-%s-+' % ('-' * tag_len)
732 tag_header = '| %s |' % tag_header.ljust(tag_len)
733 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
734 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400735
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000736 if include_stdout:
737 return '\n'.join([
738 dash_pad,
739 tag_header,
740 dash_pad,
Marc-Antoine Ruel6bb4fb32017-11-03 14:34:49 -0400741 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000742 dash_pad,
743 tag_footer1,
744 tag_footer2,
745 dash_pad,
746 ])
747 else:
748 return '\n'.join([
749 dash_pad,
750 tag_header,
751 tag_footer2,
752 dash_pad,
753 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000754
755
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700756def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700757 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000758 task_summary_json, task_output_dir, task_output_stdout,
759 include_perf):
maruela5490782015-09-30 10:56:59 -0700760 """Retrieves results of a Swarming task.
761
762 Returns:
763 process exit code that should be returned to the user.
764 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700765 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000766 output_collector = TaskOutputCollector(
767 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700768
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700769 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700770 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400771 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700772 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400773 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400774 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000775 output_collector, include_perf,
776 (len(task_output_stdout) > 0),
777 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700778 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700779
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400780 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700781 shard_exit_code = metadata.get('exit_code')
782 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700783 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700784 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700785 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400786 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700787 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700788
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700789 if decorate:
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000790 s = decorate_shard_output(
791 swarming, index, metadata,
792 "console" in task_output_stdout).encode(
793 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700794 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400795 if len(seen_shards) < len(task_ids):
796 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700797 else:
maruel77f720b2015-09-15 12:35:22 -0700798 print('%s: %s %s' % (
799 metadata.get('bot_id', 'N/A'),
800 metadata['task_id'],
801 shard_exit_code))
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000802 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700803 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400804 if output:
805 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700806 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700807 summary = output_collector.finalize()
808 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700809 # TODO(maruel): Make this optional.
810 for i in summary['shards']:
811 if i:
812 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700813 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700814
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400815 if decorate and total_duration:
816 print('Total duration: %.1fs' % total_duration)
817
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400818 if len(seen_shards) != len(task_ids):
819 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700820 print >> sys.stderr, ('Results from some shards are missing: %s' %
821 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700822 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700823
maruela5490782015-09-30 10:56:59 -0700824 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000825
826
maruel77f720b2015-09-15 12:35:22 -0700827### API management.
828
829
830class APIError(Exception):
831 pass
832
833
834def endpoints_api_discovery_apis(host):
835 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
836 the APIs exposed by a host.
837
838 https://developers.google.com/discovery/v1/reference/apis/list
839 """
maruel380e3262016-08-31 16:10:06 -0700840 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
841 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700842 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
843 if data is None:
844 raise APIError('Failed to discover APIs on %s' % host)
845 out = {}
846 for api in data['items']:
847 if api['id'] == 'discovery:v1':
848 continue
849 # URL is of the following form:
850 # url = host + (
851 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
852 api_data = net.url_read_json(api['discoveryRestUrl'])
853 if api_data is None:
854 raise APIError('Failed to discover %s on %s' % (api['id'], host))
855 out[api['id']] = api_data
856 return out
857
858
maruel0165e822017-06-08 06:26:53 -0700859def get_yielder(base_url, limit):
860 """Returns the first query and a function that yields following items."""
861 CHUNK_SIZE = 250
862
863 url = base_url
864 if limit:
865 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
866 data = net.url_read_json(url)
867 if data is None:
868 # TODO(maruel): Do basic diagnostic.
869 raise Failure('Failed to access %s' % url)
870 org_cursor = data.pop('cursor', None)
871 org_total = len(data.get('items') or [])
872 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
873 if not org_cursor or not org_total:
874 # This is not an iterable resource.
875 return data, lambda: []
876
877 def yielder():
878 cursor = org_cursor
879 total = org_total
880 # Some items support cursors. Try to get automatically if cursors are needed
881 # by looking at the 'cursor' items.
882 while cursor and (not limit or total < limit):
883 merge_char = '&' if '?' in base_url else '?'
884 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
885 if limit:
886 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
887 new = net.url_read_json(url)
888 if new is None:
889 raise Failure('Failed to access %s' % url)
890 cursor = new.get('cursor')
891 new_items = new.get('items')
892 nb_items = len(new_items or [])
893 total += nb_items
894 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
895 yield new_items
896
897 return data, yielder
898
899
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500900### Commands.
901
902
903def abort_task(_swarming, _manifest):
904 """Given a task manifest that was triggered, aborts its execution."""
905 # TODO(vadimsh): No supported by the server yet.
906
907
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400908def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800909 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500910 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500911 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500912 dest='dimensions', metavar='FOO bar',
913 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500914 parser.add_option_group(parser.filter_group)
915
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400916
maruel0165e822017-06-08 06:26:53 -0700917def process_filter_options(parser, options):
918 for key, value in options.dimensions:
919 if ':' in key:
920 parser.error('--dimension key cannot contain ":"')
921 if key.strip() != key:
922 parser.error('--dimension key has whitespace')
923 if not key:
924 parser.error('--dimension key is empty')
925
926 if value.strip() != value:
927 parser.error('--dimension value has whitespace')
928 if not value:
929 parser.error('--dimension value is empty')
930 options.dimensions.sort()
931
932
Vadim Shtayurab450c602014-05-12 19:23:25 -0700933def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400934 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700935 parser.sharding_group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700936 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700937 help='Number of shards to trigger and collect.')
938 parser.add_option_group(parser.sharding_group)
939
940
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400941def add_trigger_options(parser):
942 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500943 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400944 add_filter_options(parser)
945
maruel681d6802017-01-17 16:56:03 -0800946 group = optparse.OptionGroup(parser, 'Task properties')
947 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700948 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500949 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800950 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500951 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700952 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800953 group.add_option(
Robert Iannuccibe66ce72017-11-22 12:56:50 -0800954 '--env-prefix', default=[], action='append', nargs=2,
955 metavar='VAR local/path',
956 help='Prepend task-relative `local/path` to the task\'s VAR environment '
957 'variable using os-appropriate pathsep character. Can be specified '
958 'multiple times for the same VAR to add multiple paths.')
959 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400960 '--idempotent', action='store_true', default=False,
961 help='When set, the server will actively try to find a previous task '
962 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800963 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700964 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700965 help='The optional path to a file containing the secret_bytes to use with'
966 'this task.')
maruel681d6802017-01-17 16:56:03 -0800967 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700968 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400969 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800970 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700971 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400972 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800973 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500974 '--raw-cmd', action='store_true', default=False,
975 help='When set, the command after -- is used as-is without run_isolated. '
maruela9fe2cb2017-05-10 10:43:23 -0700976 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800977 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700978 '--cipd-package', action='append', default=[], metavar='PKG',
979 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700980 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800981 group.add_option(
982 '--named-cache', action='append', nargs=2, default=[],
maruel3773d8c2017-05-31 15:35:47 -0700983 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800984 help='"<name> <relpath>" items to keep a persistent bot managed cache')
985 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700986 '--service-account',
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700987 help='Email of a service account to run the task as, or literal "bot" '
988 'string to indicate that the task should use the same account the '
989 'bot itself is using to authenticate to Swarming. Don\'t use task '
990 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800991 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700992 '-o', '--output', action='append', default=[], metavar='PATH',
993 help='A list of files to return in addition to those written to '
994 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
995 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -0800996 parser.add_option_group(group)
997
998 group = optparse.OptionGroup(parser, 'Task request')
999 group.add_option(
1000 '--priority', type='int', default=100,
1001 help='The lower value, the more important the task is')
1002 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -07001003 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001004 help='Display name of the task. Defaults to '
1005 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1006 'isolated file is provided, if a hash is provided, it defaults to '
1007 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1008 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -07001009 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001010 help='Tags to assign to the task.')
1011 group.add_option(
1012 '--user', default='',
1013 help='User associated with the task. Defaults to authenticated user on '
1014 'the server.')
1015 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -07001016 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001017 help='Seconds to allow the task to be pending for a bot to run before '
1018 'this task request expires.')
1019 group.add_option(
1020 '--deadline', type='int', dest='expiration',
1021 help=optparse.SUPPRESS_HELP)
1022 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001023
1024
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001025def process_trigger_options(parser, options, args):
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001026 """Processes trigger options and does preparatory steps."""
maruel0165e822017-06-08 06:26:53 -07001027 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001028 options.env = dict(options.env)
maruela9fe2cb2017-05-10 10:43:23 -07001029 if args and args[0] == '--':
1030 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001031
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001032 if not options.dimensions:
1033 parser.error('Please at least specify one --dimension')
maruela9fe2cb2017-05-10 10:43:23 -07001034 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1035 parser.error('--tags must be in the format key:value')
1036 if options.raw_cmd and not args:
1037 parser.error(
1038 'Arguments with --raw-cmd should be passed after -- as command '
1039 'delimiter.')
1040 if options.isolate_server and not options.namespace:
1041 parser.error(
1042 '--namespace must be a valid value when --isolate-server is used')
1043 if not options.isolated and not options.raw_cmd:
1044 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1045
1046 # Isolated
1047 # --isolated is required only if --raw-cmd wasn't provided.
1048 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1049 # preferred server.
1050 isolateserver.process_isolate_server_options(
1051 parser, options, False, not options.raw_cmd)
1052 inputs_ref = None
1053 if options.isolate_server:
1054 inputs_ref = FilesRef(
1055 isolated=options.isolated,
1056 isolatedserver=options.isolate_server,
1057 namespace=options.namespace)
1058
1059 # Command
1060 command = None
1061 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001062 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001063 command = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001064 else:
maruela9fe2cb2017-05-10 10:43:23 -07001065 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001066
maruela9fe2cb2017-05-10 10:43:23 -07001067 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001068 cipd_packages = []
1069 for p in options.cipd_package:
1070 split = p.split(':', 2)
1071 if len(split) != 3:
1072 parser.error('CIPD packages must take the form: path:package:version')
1073 cipd_packages.append(CipdPackage(
1074 package_name=split[1],
1075 path=split[0],
1076 version=split[2]))
1077 cipd_input = None
1078 if cipd_packages:
1079 cipd_input = CipdInput(
1080 client_package=None,
1081 packages=cipd_packages,
1082 server=None)
1083
maruela9fe2cb2017-05-10 10:43:23 -07001084 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001085 secret_bytes = None
1086 if options.secret_bytes_path:
1087 with open(options.secret_bytes_path, 'r') as f:
1088 secret_bytes = f.read().encode('base64')
1089
maruela9fe2cb2017-05-10 10:43:23 -07001090 # Named caches
maruel681d6802017-01-17 16:56:03 -08001091 caches = [
1092 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1093 for i in options.named_cache
1094 ]
maruela9fe2cb2017-05-10 10:43:23 -07001095
Robert Iannuccibe66ce72017-11-22 12:56:50 -08001096 env_prefixes = {}
1097 for k, v in options.env_prefix:
1098 env_prefixes.setdefault(k, []).append(v)
1099
maruel77f720b2015-09-15 12:35:22 -07001100 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001101 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001102 cipd_input=cipd_input,
maruela9fe2cb2017-05-10 10:43:23 -07001103 command=command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001104 dimensions=options.dimensions,
1105 env=options.env,
Robert Iannuccibe66ce72017-11-22 12:56:50 -08001106 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001107 execution_timeout_secs=options.hard_timeout,
maruela9fe2cb2017-05-10 10:43:23 -07001108 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001109 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001110 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001111 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001112 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001113 outputs=options.output,
1114 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001115
maruel77f720b2015-09-15 12:35:22 -07001116 return NewTaskRequest(
1117 expiration_secs=options.expiration,
maruela9fe2cb2017-05-10 10:43:23 -07001118 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001119 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001120 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001121 properties=properties,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001122 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001123 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001124 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001125
1126
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001127class TaskOutputStdoutOption(optparse.Option):
1128 """Where to output the each task's console output (stderr/stdout).
1129
1130 The output will be;
1131 none - not be downloaded.
1132 json - stored in summary.json file *only*.
1133 console - shown on stdout *only*.
1134 all - stored in summary.json and shown on stdout.
1135 """
1136
1137 choices = ['all', 'json', 'console', 'none']
1138
1139 def __init__(self, *args, **kw):
1140 optparse.Option.__init__(
1141 self,
1142 *args,
1143 choices=self.choices,
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001144 default=['console', 'json'],
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001145 help=re.sub('\s\s*', ' ', self.__doc__),
1146 **kw)
1147
1148 def convert_value(self, opt, value):
1149 if value not in self.choices:
1150 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1151 self.get_opt_string(), self.choices, value))
1152 stdout_to = []
1153 if value == 'all':
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001154 stdout_to = ['console', 'json']
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001155 elif value != 'none':
1156 stdout_to = [value]
1157 return stdout_to
1158
1159
maruel@chromium.org0437a732013-08-27 16:05:52 +00001160def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001161 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001162 '-t', '--timeout', type='float',
1163 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1164 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001165 parser.group_logging.add_option(
1166 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001167 parser.group_logging.add_option(
1168 '--print-status-updates', action='store_true',
1169 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001170 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001171 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001172 '--task-summary-json',
1173 metavar='FILE',
1174 help='Dump a summary of task results to this file as json. It contains '
1175 'only shards statuses as know to server directly. Any output files '
1176 'emitted by the task can be collected by using --task-output-dir')
1177 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001178 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001179 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001180 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001181 'directory contains per-shard directory with output files produced '
1182 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001183 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001184 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001185 parser.task_output_group.add_option(
1186 '--perf', action='store_true', default=False,
1187 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001188 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001189
1190
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001191@subcommand.usage('bots...')
1192def CMDbot_delete(parser, args):
1193 """Forcibly deletes bots from the Swarming server."""
1194 parser.add_option(
1195 '-f', '--force', action='store_true',
1196 help='Do not prompt for confirmation')
1197 options, args = parser.parse_args(args)
1198 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001199 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001200
1201 bots = sorted(args)
1202 if not options.force:
1203 print('Delete the following bots?')
1204 for bot in bots:
1205 print(' %s' % bot)
1206 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1207 print('Goodbye.')
1208 return 1
1209
1210 result = 0
1211 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001212 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001213 if net.url_read_json(url, data={}, method='POST') is None:
1214 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001215 result = 1
1216 return result
1217
1218
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001219def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001220 """Returns information about the bots connected to the Swarming server."""
1221 add_filter_options(parser)
1222 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001223 '--dead-only', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001224 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001225 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001226 '-k', '--keep-dead', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001227 help='Keep both dead and alive bots')
1228 parser.filter_group.add_option(
1229 '--busy', action='store_true', help='Keep only busy bots')
1230 parser.filter_group.add_option(
1231 '--idle', action='store_true', help='Keep only idle bots')
1232 parser.filter_group.add_option(
1233 '--mp', action='store_true',
1234 help='Keep only Machine Provider managed bots')
1235 parser.filter_group.add_option(
1236 '--non-mp', action='store_true',
1237 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001238 parser.filter_group.add_option(
1239 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001240 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001241 options, args = parser.parse_args(args)
maruel0165e822017-06-08 06:26:53 -07001242 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001243
1244 if options.keep_dead and options.dead_only:
maruel0165e822017-06-08 06:26:53 -07001245 parser.error('Use only one of --keep-dead or --dead-only')
1246 if options.busy and options.idle:
1247 parser.error('Use only one of --busy or --idle')
1248 if options.mp and options.non_mp:
1249 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001250
maruel0165e822017-06-08 06:26:53 -07001251 url = options.swarming + '/api/swarming/v1/bots/list?'
1252 values = []
1253 if options.dead_only:
1254 values.append(('is_dead', 'TRUE'))
1255 elif options.keep_dead:
1256 values.append(('is_dead', 'NONE'))
1257 else:
1258 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001259
maruel0165e822017-06-08 06:26:53 -07001260 if options.busy:
1261 values.append(('is_busy', 'TRUE'))
1262 elif options.idle:
1263 values.append(('is_busy', 'FALSE'))
1264 else:
1265 values.append(('is_busy', 'NONE'))
1266
1267 if options.mp:
1268 values.append(('is_mp', 'TRUE'))
1269 elif options.non_mp:
1270 values.append(('is_mp', 'FALSE'))
1271 else:
1272 values.append(('is_mp', 'NONE'))
1273
1274 for key, value in options.dimensions:
1275 values.append(('dimensions', '%s:%s' % (key, value)))
1276 url += urllib.urlencode(values)
1277 try:
1278 data, yielder = get_yielder(url, 0)
1279 bots = data.get('items') or []
1280 for items in yielder():
1281 if items:
1282 bots.extend(items)
1283 except Failure as e:
1284 sys.stderr.write('\n%s\n' % e)
1285 return 1
maruel77f720b2015-09-15 12:35:22 -07001286 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruel0165e822017-06-08 06:26:53 -07001287 print bot['bot_id']
1288 if not options.bare:
1289 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1290 print ' %s' % json.dumps(dimensions, sort_keys=True)
1291 if bot.get('task_id'):
1292 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001293 return 0
1294
1295
maruelfd0a90c2016-06-10 11:51:10 -07001296@subcommand.usage('task_id')
1297def CMDcancel(parser, args):
1298 """Cancels a task."""
1299 options, args = parser.parse_args(args)
1300 if not args:
1301 parser.error('Please specify the task to cancel')
1302 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001303 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001304 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1305 print('Deleting %s failed. Probably already gone' % task_id)
1306 return 1
1307 return 0
1308
1309
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001310@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001311def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001312 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001313
1314 The result can be in multiple part if the execution was sharded. It can
1315 potentially have retries.
1316 """
1317 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001318 parser.add_option(
1319 '-j', '--json',
1320 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001321 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001322 if not args and not options.json:
1323 parser.error('Must specify at least one task id or --json.')
1324 if args and options.json:
1325 parser.error('Only use one of task id or --json.')
1326
1327 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001328 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001329 try:
maruel1ceb3872015-10-14 06:10:44 -07001330 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001331 data = json.load(f)
1332 except (IOError, ValueError):
1333 parser.error('Failed to open %s' % options.json)
1334 try:
1335 tasks = sorted(
1336 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1337 args = [t['task_id'] for t in tasks]
1338 except (KeyError, TypeError):
1339 parser.error('Failed to process %s' % options.json)
1340 if options.timeout is None:
1341 options.timeout = (
1342 data['request']['properties']['execution_timeout_secs'] +
1343 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001344 else:
1345 valid = frozenset('0123456789abcdef')
1346 if any(not valid.issuperset(task_id) for task_id in args):
1347 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001348
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001349 try:
1350 return collect(
1351 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001352 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001353 options.timeout,
1354 options.decorate,
1355 options.print_status_updates,
1356 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001357 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001358 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001359 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001360 except Failure:
1361 on_error.report(None)
1362 return 1
1363
1364
maruel77f720b2015-09-15 12:35:22 -07001365@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001366def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001367 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1368 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001369
1370 Examples:
maruel0165e822017-06-08 06:26:53 -07001371 Raw task request and results:
1372 swarming.py query -S server-url.com task/123456/request
1373 swarming.py query -S server-url.com task/123456/result
1374
maruel77f720b2015-09-15 12:35:22 -07001375 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001376 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001377
maruel0165e822017-06-08 06:26:53 -07001378 Listing last 10 tasks on a specific bot named 'bot1':
1379 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001380
maruel0165e822017-06-08 06:26:53 -07001381 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001382 quoting is important!:
1383 swarming.py query -S server-url.com --limit 10 \\
maruel0165e822017-06-08 06:26:53 -07001384 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001385 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001386 parser.add_option(
1387 '-L', '--limit', type='int', default=200,
1388 help='Limit to enforce on limitless items (like number of tasks); '
1389 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001390 parser.add_option(
1391 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001392 parser.add_option(
1393 '--progress', action='store_true',
1394 help='Prints a dot at each request to show progress')
1395 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001396 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001397 parser.error(
1398 'Must specify only method name and optionally query args properly '
1399 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001400 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruel0165e822017-06-08 06:26:53 -07001401 try:
1402 data, yielder = get_yielder(base_url, options.limit)
1403 for items in yielder():
1404 if items:
1405 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001406 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001407 sys.stderr.write('.')
1408 sys.stderr.flush()
1409 except Failure as e:
1410 sys.stderr.write('\n%s\n' % e)
1411 return 1
maruel77f720b2015-09-15 12:35:22 -07001412 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001413 sys.stderr.write('\n')
1414 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001415 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001416 options.json = unicode(os.path.abspath(options.json))
1417 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001418 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001419 try:
maruel77f720b2015-09-15 12:35:22 -07001420 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001421 sys.stdout.write('\n')
1422 except IOError:
1423 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001424 return 0
1425
1426
maruel77f720b2015-09-15 12:35:22 -07001427def CMDquery_list(parser, args):
1428 """Returns list of all the Swarming APIs that can be used with command
1429 'query'.
1430 """
1431 parser.add_option(
1432 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1433 options, args = parser.parse_args(args)
1434 if args:
1435 parser.error('No argument allowed.')
1436
1437 try:
1438 apis = endpoints_api_discovery_apis(options.swarming)
1439 except APIError as e:
1440 parser.error(str(e))
1441 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001442 options.json = unicode(os.path.abspath(options.json))
1443 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001444 json.dump(apis, f)
1445 else:
1446 help_url = (
1447 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1448 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001449 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1450 if i:
1451 print('')
maruel77f720b2015-09-15 12:35:22 -07001452 print api_id
maruel11e31af2017-02-15 07:30:50 -08001453 print ' ' + api['description'].strip()
1454 if 'resources' in api:
1455 # Old.
1456 for j, (resource_name, resource) in enumerate(
1457 sorted(api['resources'].iteritems())):
1458 if j:
1459 print('')
1460 for method_name, method in sorted(resource['methods'].iteritems()):
1461 # Only list the GET ones.
1462 if method['httpMethod'] != 'GET':
1463 continue
1464 print '- %s.%s: %s' % (
1465 resource_name, method_name, method['path'])
1466 print('\n'.join(
1467 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1468 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1469 else:
1470 # New.
1471 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001472 # Only list the GET ones.
1473 if method['httpMethod'] != 'GET':
1474 continue
maruel11e31af2017-02-15 07:30:50 -08001475 print '- %s: %s' % (method['id'], method['path'])
1476 print('\n'.join(
1477 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001478 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1479 return 0
1480
1481
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001482@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001483def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001484 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001485
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001486 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001487 """
1488 add_trigger_options(parser)
1489 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001490 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001491 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001492 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001493 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001494 tasks = trigger_task_shards(
1495 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001496 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001497 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001498 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001499 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001500 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001501 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001502 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001503 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001504 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001505 task_ids = [
1506 t['task_id']
1507 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1508 ]
maruel71c61c82016-02-22 06:52:05 -08001509 if options.timeout is None:
1510 options.timeout = (
1511 task_request.properties.execution_timeout_secs +
1512 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001513 try:
1514 return collect(
1515 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001516 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001517 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001518 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001519 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001520 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001521 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001522 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001523 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001524 except Failure:
1525 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001526 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001527
1528
maruel18122c62015-10-23 06:31:23 -07001529@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001530def CMDreproduce(parser, args):
1531 """Runs a task locally that was triggered on the server.
1532
1533 This running locally the same commands that have been run on the bot. The data
1534 downloaded will be in a subdirectory named 'work' of the current working
1535 directory.
maruel18122c62015-10-23 06:31:23 -07001536
1537 You can pass further additional arguments to the target command by passing
1538 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001539 """
maruelc070e672016-02-22 17:32:57 -08001540 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001541 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001542 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001543 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001544 extra_args = []
1545 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001546 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001547 if len(args) > 1:
1548 if args[1] == '--':
1549 if len(args) > 2:
1550 extra_args = args[2:]
1551 else:
1552 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001553
maruel380e3262016-08-31 16:10:06 -07001554 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001555 request = net.url_read_json(url)
1556 if not request:
1557 print >> sys.stderr, 'Failed to retrieve request data for the task'
1558 return 1
1559
maruel12e30012015-10-09 11:55:35 -07001560 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001561 if fs.isdir(workdir):
1562 parser.error('Please delete the directory \'work\' first')
1563 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001564 cachedir = unicode(os.path.abspath('cipd_cache'))
1565 if not fs.exists(cachedir):
1566 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001567
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001568 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001569 env = os.environ.copy()
1570 env['SWARMING_BOT_ID'] = 'reproduce'
1571 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001572 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001573 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001574 for i in properties['env']:
1575 key = i['key'].encode('utf-8')
1576 if not i['value']:
1577 env.pop(key, None)
1578 else:
1579 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001580
Robert Iannuccibe66ce72017-11-22 12:56:50 -08001581 if properties.get('env_prefixes'):
1582 env_prefixes = properties['env']
1583 logging.info('env_prefixes: %r', env_prefixes)
1584 for key, paths in env_prefixes.iteritems():
1585 paths = [os.path.normpath(os.path.join(workdir, p)) for p in paths]
1586 cur = env.get(key)
1587 if cur:
1588 paths.append(cur)
1589 env[key] = os.path.pathsep.join(paths)
1590
iannucci31ab9192017-05-02 19:11:56 -07001591 command = []
nodir152cba62016-05-12 16:08:56 -07001592 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001593 # Create the tree.
1594 with isolateserver.get_storage(
1595 properties['inputs_ref']['isolatedserver'],
1596 properties['inputs_ref']['namespace']) as storage:
1597 bundle = isolateserver.fetch_isolated(
1598 properties['inputs_ref']['isolated'],
1599 storage,
1600 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001601 workdir,
1602 False)
maruel29ab2fd2015-10-16 11:44:01 -07001603 command = bundle.command
1604 if bundle.relative_cwd:
1605 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001606 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001607
1608 if properties.get('command'):
1609 command.extend(properties['command'])
1610
1611 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1612 new_command = tools.fix_python_path(command)
1613 new_command = run_isolated.process_command(
1614 new_command, options.output_dir, None)
1615 if not options.output_dir and new_command != command:
1616 parser.error('The task has outputs, you must use --output-dir')
1617 command = new_command
1618 file_path.ensure_command_has_abs_path(command, workdir)
1619
1620 if properties.get('cipd_input'):
1621 ci = properties['cipd_input']
1622 cp = ci['client_package']
1623 client_manager = cipd.get_client(
1624 ci['server'], cp['package_name'], cp['version'], cachedir)
1625
1626 with client_manager as client:
1627 by_path = collections.defaultdict(list)
1628 for pkg in ci['packages']:
1629 path = pkg['path']
1630 # cipd deals with 'root' as ''
1631 if path == '.':
1632 path = ''
1633 by_path[path].append((pkg['package_name'], pkg['version']))
1634 client.ensure(workdir, by_path, cache_dir=cachedir)
1635
maruel77f720b2015-09-15 12:35:22 -07001636 try:
maruel18122c62015-10-23 06:31:23 -07001637 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001638 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001639 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001640 print >> sys.stderr, str(e)
1641 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001642
1643
maruel0eb1d1b2015-10-02 14:48:21 -07001644@subcommand.usage('bot_id')
1645def CMDterminate(parser, args):
1646 """Tells a bot to gracefully shut itself down as soon as it can.
1647
1648 This is done by completing whatever current task there is then exiting the bot
1649 process.
1650 """
1651 parser.add_option(
1652 '--wait', action='store_true', help='Wait for the bot to terminate')
1653 options, args = parser.parse_args(args)
1654 if len(args) != 1:
1655 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001656 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001657 request = net.url_read_json(url, data={})
1658 if not request:
1659 print >> sys.stderr, 'Failed to ask for termination'
1660 return 1
1661 if options.wait:
1662 return collect(
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001663 options.swarming,
1664 [request['task_id']],
1665 0.,
1666 False,
1667 False,
1668 None,
1669 None,
1670 [],
maruel9531ce02016-04-13 06:11:23 -07001671 False)
maruelb7ded002017-06-10 16:43:17 -07001672 else:
1673 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001674 return 0
1675
1676
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001677@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001678def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001679 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001680
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001681 Passes all extra arguments provided after '--' as additional command line
1682 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001683 """
1684 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001685 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001686 parser.add_option(
1687 '--dump-json',
1688 metavar='FILE',
1689 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001690 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001691 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001692 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001693 tasks = trigger_task_shards(
1694 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001695 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001696 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001697 tasks_sorted = sorted(
1698 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001699 if options.dump_json:
1700 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001701 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001702 'tasks': tasks,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001703 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001704 }
maruel46b015f2015-10-13 18:40:35 -07001705 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001706 print('To collect results, use:')
1707 print(' swarming.py collect -S %s --json %s' %
1708 (options.swarming, options.dump_json))
1709 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001710 print('To collect results, use:')
1711 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001712 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1713 print('Or visit:')
1714 for t in tasks_sorted:
1715 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001716 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001717 except Failure:
1718 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001719 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001720
1721
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001722class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001723 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001724 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001725 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001726 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001727 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001728 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001729 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001730 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001731 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001732 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001733
1734 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001735 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001736 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001737 auth.process_auth_options(self, options)
1738 user = self._process_swarming(options)
1739 if hasattr(options, 'user') and not options.user:
1740 options.user = user
1741 return options, args
1742
1743 def _process_swarming(self, options):
1744 """Processes the --swarming option and aborts if not specified.
1745
1746 Returns the identity as determined by the server.
1747 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001748 if not options.swarming:
1749 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001750 try:
1751 options.swarming = net.fix_url(options.swarming)
1752 except ValueError as e:
1753 self.error('--swarming %s' % e)
1754 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001755 try:
1756 user = auth.ensure_logged_in(options.swarming)
1757 except ValueError as e:
1758 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001759 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001760
1761
1762def main(args):
1763 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001764 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001765
1766
1767if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001768 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001769 fix_encoding.fix_encoding()
1770 tools.disable_buffering()
1771 colorama.init()
1772 sys.exit(main(sys.argv[1:]))