blob: f9a5d25183143b2a8a8016c3ec80e53f02618c24 [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 Ruel95c21872018-01-10 14:24:28 -05008__version__ = '0.10.2'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import sys
maruel11e31af2017-02-15 07:30:50 -080018import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
iannucci31ab9192017-05-02 19:11:56 -070038import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
maruelc070e672016-02-22 17:32:57 -080040import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000041
42
tansella4949442016-06-23 22:34:32 -070043ROOT_DIR = os.path.dirname(os.path.abspath(
44 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050045
46
47class Failure(Exception):
48 """Generic failure."""
49 pass
50
51
maruel0a25f6c2017-05-10 10:43:23 -070052def default_task_name(options):
53 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050054 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070055 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070056 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070057 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070058 if options.isolated:
59 task_name += u'/' + options.isolated
60 return task_name
61 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050062
63
64### Triggering.
65
66
maruel77f720b2015-09-15 12:35:22 -070067# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070068CipdPackage = collections.namedtuple(
69 'CipdPackage',
70 [
71 'package_name',
72 'path',
73 'version',
74 ])
75
76
77# See ../appengine/swarming/swarming_rpcs.py.
78CipdInput = collections.namedtuple(
79 'CipdInput',
80 [
81 'client_package',
82 'packages',
83 'server',
84 ])
85
86
87# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070088FilesRef = collections.namedtuple(
89 'FilesRef',
90 [
91 'isolated',
92 'isolatedserver',
93 'namespace',
94 ])
95
96
97# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080098StringListPair = collections.namedtuple(
99 'StringListPair', [
100 'key',
101 'value', # repeated string
102 ]
103)
104
105
106# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700107TaskProperties = collections.namedtuple(
108 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500109 [
maruel681d6802017-01-17 16:56:03 -0800110 'caches',
borenet02f772b2016-06-22 12:42:19 -0700111 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500112 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500113 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500114 'dimensions',
115 'env',
Robert Iannuccibf5f84c2017-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 Shtayura2d83a942017-08-14 17:41:24 -0700137 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500138 'tags',
139 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500140 ])
141
142
maruel77f720b2015-09-15 12:35:22 -0700143def namedtuple_to_dict(value):
144 """Recursively converts a namedtuple to a dict."""
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 Shtayura2d83a942017-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 Shtayura2d83a942017-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}
maruelaf6b06c2017-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 Shtayura2d83a942017-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 Ruelb1216762017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-09-08 09:32:52 +1000553 if fetch_stdout:
554 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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 Ruel3f9931a2017-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' Ansell5e8001d2017-09-08 09:32:52 +1000718 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-09-08 09:32:52 +1000736 if include_stdout:
737 return '\n'.join([
738 dash_pad,
739 tag_header,
740 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400741 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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' Ansell5e8001d2017-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
maruelaf6b06c2017-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
maruelaf6b06c2017-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(
maruel5475ba62017-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(
maruel5475ba62017-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 Iannuccibf5f84c2017-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(
maruel5475ba62017-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(
maruel5475ba62017-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(
maruel5475ba62017-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. '
maruel0a25f6c2017-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(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500978 '--relative-cwd',
979 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
980 'requires --raw-cmd')
981 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700982 '--cipd-package', action='append', default=[], metavar='PKG',
983 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700984 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800985 group.add_option(
986 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700987 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800988 help='"<name> <relpath>" items to keep a persistent bot managed cache')
989 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700990 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700991 help='Email of a service account to run the task as, or literal "bot" '
992 'string to indicate that the task should use the same account the '
993 'bot itself is using to authenticate to Swarming. Don\'t use task '
994 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800995 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700996 '-o', '--output', action='append', default=[], metavar='PATH',
997 help='A list of files to return in addition to those written to '
998 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
999 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -08001000 parser.add_option_group(group)
1001
1002 group = optparse.OptionGroup(parser, 'Task request')
1003 group.add_option(
1004 '--priority', type='int', default=100,
1005 help='The lower value, the more important the task is')
1006 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001007 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001008 help='Display name of the task. Defaults to '
1009 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1010 'isolated file is provided, if a hash is provided, it defaults to '
1011 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1012 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001013 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001014 help='Tags to assign to the task.')
1015 group.add_option(
1016 '--user', default='',
1017 help='User associated with the task. Defaults to authenticated user on '
1018 'the server.')
1019 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001020 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001021 help='Seconds to allow the task to be pending for a bot to run before '
1022 'this task request expires.')
1023 group.add_option(
1024 '--deadline', type='int', dest='expiration',
1025 help=optparse.SUPPRESS_HELP)
1026 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001027
1028
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001029def process_trigger_options(parser, options, args):
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001030 """Processes trigger options and does preparatory steps."""
maruelaf6b06c2017-06-08 06:26:53 -07001031 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001032 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001033 if args and args[0] == '--':
1034 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001035
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001036 if not options.dimensions:
1037 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001038 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1039 parser.error('--tags must be in the format key:value')
1040 if options.raw_cmd and not args:
1041 parser.error(
1042 'Arguments with --raw-cmd should be passed after -- as command '
1043 'delimiter.')
1044 if options.isolate_server and not options.namespace:
1045 parser.error(
1046 '--namespace must be a valid value when --isolate-server is used')
1047 if not options.isolated and not options.raw_cmd:
1048 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1049
1050 # Isolated
1051 # --isolated is required only if --raw-cmd wasn't provided.
1052 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1053 # preferred server.
1054 isolateserver.process_isolate_server_options(
1055 parser, options, False, not options.raw_cmd)
1056 inputs_ref = None
1057 if options.isolate_server:
1058 inputs_ref = FilesRef(
1059 isolated=options.isolated,
1060 isolatedserver=options.isolate_server,
1061 namespace=options.namespace)
1062
1063 # Command
1064 command = None
1065 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001066 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001067 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001068 if options.relative_cwd:
1069 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1070 if not a.startswith(os.getcwd()):
1071 parser.error(
1072 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001073 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001074 if options.relative_cwd:
1075 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001076 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001077
maruel0a25f6c2017-05-10 10:43:23 -07001078 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001079 cipd_packages = []
1080 for p in options.cipd_package:
1081 split = p.split(':', 2)
1082 if len(split) != 3:
1083 parser.error('CIPD packages must take the form: path:package:version')
1084 cipd_packages.append(CipdPackage(
1085 package_name=split[1],
1086 path=split[0],
1087 version=split[2]))
1088 cipd_input = None
1089 if cipd_packages:
1090 cipd_input = CipdInput(
1091 client_package=None,
1092 packages=cipd_packages,
1093 server=None)
1094
maruel0a25f6c2017-05-10 10:43:23 -07001095 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001096 secret_bytes = None
1097 if options.secret_bytes_path:
1098 with open(options.secret_bytes_path, 'r') as f:
1099 secret_bytes = f.read().encode('base64')
1100
maruel0a25f6c2017-05-10 10:43:23 -07001101 # Named caches
maruel681d6802017-01-17 16:56:03 -08001102 caches = [
1103 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1104 for i in options.named_cache
1105 ]
maruel0a25f6c2017-05-10 10:43:23 -07001106
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001107 env_prefixes = {}
1108 for k, v in options.env_prefix:
1109 env_prefixes.setdefault(k, []).append(v)
1110
maruel77f720b2015-09-15 12:35:22 -07001111 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001112 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001113 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001114 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001115 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001116 dimensions=options.dimensions,
1117 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001118 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001119 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001120 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001121 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001122 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001123 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001124 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001125 outputs=options.output,
1126 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001127
maruel77f720b2015-09-15 12:35:22 -07001128 return NewTaskRequest(
1129 expiration_secs=options.expiration,
maruel0a25f6c2017-05-10 10:43:23 -07001130 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001131 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001132 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001133 properties=properties,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001134 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001135 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001136 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001137
1138
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001139class TaskOutputStdoutOption(optparse.Option):
1140 """Where to output the each task's console output (stderr/stdout).
1141
1142 The output will be;
1143 none - not be downloaded.
1144 json - stored in summary.json file *only*.
1145 console - shown on stdout *only*.
1146 all - stored in summary.json and shown on stdout.
1147 """
1148
1149 choices = ['all', 'json', 'console', 'none']
1150
1151 def __init__(self, *args, **kw):
1152 optparse.Option.__init__(
1153 self,
1154 *args,
1155 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001156 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001157 help=re.sub('\s\s*', ' ', self.__doc__),
1158 **kw)
1159
1160 def convert_value(self, opt, value):
1161 if value not in self.choices:
1162 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1163 self.get_opt_string(), self.choices, value))
1164 stdout_to = []
1165 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001166 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001167 elif value != 'none':
1168 stdout_to = [value]
1169 return stdout_to
1170
1171
maruel@chromium.org0437a732013-08-27 16:05:52 +00001172def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001173 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001174 '-t', '--timeout', type='float',
1175 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1176 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001177 parser.group_logging.add_option(
1178 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001179 parser.group_logging.add_option(
1180 '--print-status-updates', action='store_true',
1181 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001182 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001183 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001184 '--task-summary-json',
1185 metavar='FILE',
1186 help='Dump a summary of task results to this file as json. It contains '
1187 'only shards statuses as know to server directly. Any output files '
1188 'emitted by the task can be collected by using --task-output-dir')
1189 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001190 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001191 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001192 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001193 'directory contains per-shard directory with output files produced '
1194 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001195 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001196 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001197 parser.task_output_group.add_option(
1198 '--perf', action='store_true', default=False,
1199 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001200 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001201
1202
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001203@subcommand.usage('bots...')
1204def CMDbot_delete(parser, args):
1205 """Forcibly deletes bots from the Swarming server."""
1206 parser.add_option(
1207 '-f', '--force', action='store_true',
1208 help='Do not prompt for confirmation')
1209 options, args = parser.parse_args(args)
1210 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001211 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001212
1213 bots = sorted(args)
1214 if not options.force:
1215 print('Delete the following bots?')
1216 for bot in bots:
1217 print(' %s' % bot)
1218 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1219 print('Goodbye.')
1220 return 1
1221
1222 result = 0
1223 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001224 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001225 if net.url_read_json(url, data={}, method='POST') is None:
1226 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001227 result = 1
1228 return result
1229
1230
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001231def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001232 """Returns information about the bots connected to the Swarming server."""
1233 add_filter_options(parser)
1234 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001235 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001236 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001237 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001238 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001239 help='Keep both dead and alive bots')
1240 parser.filter_group.add_option(
1241 '--busy', action='store_true', help='Keep only busy bots')
1242 parser.filter_group.add_option(
1243 '--idle', action='store_true', help='Keep only idle bots')
1244 parser.filter_group.add_option(
1245 '--mp', action='store_true',
1246 help='Keep only Machine Provider managed bots')
1247 parser.filter_group.add_option(
1248 '--non-mp', action='store_true',
1249 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001250 parser.filter_group.add_option(
1251 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001252 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001253 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001254 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001255
1256 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001257 parser.error('Use only one of --keep-dead or --dead-only')
1258 if options.busy and options.idle:
1259 parser.error('Use only one of --busy or --idle')
1260 if options.mp and options.non_mp:
1261 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001262
maruelaf6b06c2017-06-08 06:26:53 -07001263 url = options.swarming + '/api/swarming/v1/bots/list?'
1264 values = []
1265 if options.dead_only:
1266 values.append(('is_dead', 'TRUE'))
1267 elif options.keep_dead:
1268 values.append(('is_dead', 'NONE'))
1269 else:
1270 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001271
maruelaf6b06c2017-06-08 06:26:53 -07001272 if options.busy:
1273 values.append(('is_busy', 'TRUE'))
1274 elif options.idle:
1275 values.append(('is_busy', 'FALSE'))
1276 else:
1277 values.append(('is_busy', 'NONE'))
1278
1279 if options.mp:
1280 values.append(('is_mp', 'TRUE'))
1281 elif options.non_mp:
1282 values.append(('is_mp', 'FALSE'))
1283 else:
1284 values.append(('is_mp', 'NONE'))
1285
1286 for key, value in options.dimensions:
1287 values.append(('dimensions', '%s:%s' % (key, value)))
1288 url += urllib.urlencode(values)
1289 try:
1290 data, yielder = get_yielder(url, 0)
1291 bots = data.get('items') or []
1292 for items in yielder():
1293 if items:
1294 bots.extend(items)
1295 except Failure as e:
1296 sys.stderr.write('\n%s\n' % e)
1297 return 1
maruel77f720b2015-09-15 12:35:22 -07001298 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001299 print bot['bot_id']
1300 if not options.bare:
1301 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1302 print ' %s' % json.dumps(dimensions, sort_keys=True)
1303 if bot.get('task_id'):
1304 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001305 return 0
1306
1307
maruelfd0a90c2016-06-10 11:51:10 -07001308@subcommand.usage('task_id')
1309def CMDcancel(parser, args):
1310 """Cancels a task."""
1311 options, args = parser.parse_args(args)
1312 if not args:
1313 parser.error('Please specify the task to cancel')
1314 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001315 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001316 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1317 print('Deleting %s failed. Probably already gone' % task_id)
1318 return 1
1319 return 0
1320
1321
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001322@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001323def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001324 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001325
1326 The result can be in multiple part if the execution was sharded. It can
1327 potentially have retries.
1328 """
1329 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001330 parser.add_option(
1331 '-j', '--json',
1332 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001333 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001334 if not args and not options.json:
1335 parser.error('Must specify at least one task id or --json.')
1336 if args and options.json:
1337 parser.error('Only use one of task id or --json.')
1338
1339 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001340 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001341 try:
maruel1ceb3872015-10-14 06:10:44 -07001342 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001343 data = json.load(f)
1344 except (IOError, ValueError):
1345 parser.error('Failed to open %s' % options.json)
1346 try:
1347 tasks = sorted(
1348 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1349 args = [t['task_id'] for t in tasks]
1350 except (KeyError, TypeError):
1351 parser.error('Failed to process %s' % options.json)
1352 if options.timeout is None:
1353 options.timeout = (
1354 data['request']['properties']['execution_timeout_secs'] +
1355 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001356 else:
1357 valid = frozenset('0123456789abcdef')
1358 if any(not valid.issuperset(task_id) for task_id in args):
1359 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001360
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001361 try:
1362 return collect(
1363 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001364 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001365 options.timeout,
1366 options.decorate,
1367 options.print_status_updates,
1368 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001369 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001370 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001371 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001372 except Failure:
1373 on_error.report(None)
1374 return 1
1375
1376
maruel77f720b2015-09-15 12:35:22 -07001377@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001378def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001379 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1380 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001381
1382 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001383 Raw task request and results:
1384 swarming.py query -S server-url.com task/123456/request
1385 swarming.py query -S server-url.com task/123456/result
1386
maruel77f720b2015-09-15 12:35:22 -07001387 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001388 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001389
maruelaf6b06c2017-06-08 06:26:53 -07001390 Listing last 10 tasks on a specific bot named 'bot1':
1391 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001392
maruelaf6b06c2017-06-08 06:26:53 -07001393 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001394 quoting is important!:
1395 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001396 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001397 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001398 parser.add_option(
1399 '-L', '--limit', type='int', default=200,
1400 help='Limit to enforce on limitless items (like number of tasks); '
1401 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001402 parser.add_option(
1403 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001404 parser.add_option(
1405 '--progress', action='store_true',
1406 help='Prints a dot at each request to show progress')
1407 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001408 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001409 parser.error(
1410 'Must specify only method name and optionally query args properly '
1411 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001412 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001413 try:
1414 data, yielder = get_yielder(base_url, options.limit)
1415 for items in yielder():
1416 if items:
1417 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001418 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001419 sys.stderr.write('.')
1420 sys.stderr.flush()
1421 except Failure as e:
1422 sys.stderr.write('\n%s\n' % e)
1423 return 1
maruel77f720b2015-09-15 12:35:22 -07001424 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001425 sys.stderr.write('\n')
1426 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001427 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001428 options.json = unicode(os.path.abspath(options.json))
1429 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001430 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001431 try:
maruel77f720b2015-09-15 12:35:22 -07001432 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001433 sys.stdout.write('\n')
1434 except IOError:
1435 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001436 return 0
1437
1438
maruel77f720b2015-09-15 12:35:22 -07001439def CMDquery_list(parser, args):
1440 """Returns list of all the Swarming APIs that can be used with command
1441 'query'.
1442 """
1443 parser.add_option(
1444 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1445 options, args = parser.parse_args(args)
1446 if args:
1447 parser.error('No argument allowed.')
1448
1449 try:
1450 apis = endpoints_api_discovery_apis(options.swarming)
1451 except APIError as e:
1452 parser.error(str(e))
1453 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001454 options.json = unicode(os.path.abspath(options.json))
1455 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001456 json.dump(apis, f)
1457 else:
1458 help_url = (
1459 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1460 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001461 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1462 if i:
1463 print('')
maruel77f720b2015-09-15 12:35:22 -07001464 print api_id
maruel11e31af2017-02-15 07:30:50 -08001465 print ' ' + api['description'].strip()
1466 if 'resources' in api:
1467 # Old.
1468 for j, (resource_name, resource) in enumerate(
1469 sorted(api['resources'].iteritems())):
1470 if j:
1471 print('')
1472 for method_name, method in sorted(resource['methods'].iteritems()):
1473 # Only list the GET ones.
1474 if method['httpMethod'] != 'GET':
1475 continue
1476 print '- %s.%s: %s' % (
1477 resource_name, method_name, method['path'])
1478 print('\n'.join(
1479 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1480 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1481 else:
1482 # New.
1483 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001484 # Only list the GET ones.
1485 if method['httpMethod'] != 'GET':
1486 continue
maruel11e31af2017-02-15 07:30:50 -08001487 print '- %s: %s' % (method['id'], method['path'])
1488 print('\n'.join(
1489 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001490 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1491 return 0
1492
1493
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001494@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001495def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001496 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001497
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001498 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001499 """
1500 add_trigger_options(parser)
1501 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001502 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001503 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001504 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001505 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001506 tasks = trigger_task_shards(
1507 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001508 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001509 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001510 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001511 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001512 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001513 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001514 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001515 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001516 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001517 task_ids = [
1518 t['task_id']
1519 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1520 ]
maruel71c61c82016-02-22 06:52:05 -08001521 if options.timeout is None:
1522 options.timeout = (
1523 task_request.properties.execution_timeout_secs +
1524 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001525 try:
1526 return collect(
1527 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001528 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001529 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001530 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001531 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001532 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001533 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001534 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001535 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001536 except Failure:
1537 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001538 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001539
1540
maruel18122c62015-10-23 06:31:23 -07001541@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001542def CMDreproduce(parser, args):
1543 """Runs a task locally that was triggered on the server.
1544
1545 This running locally the same commands that have been run on the bot. The data
1546 downloaded will be in a subdirectory named 'work' of the current working
1547 directory.
maruel18122c62015-10-23 06:31:23 -07001548
1549 You can pass further additional arguments to the target command by passing
1550 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001551 """
maruelc070e672016-02-22 17:32:57 -08001552 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001553 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001554 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001555 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001556 extra_args = []
1557 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001558 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001559 if len(args) > 1:
1560 if args[1] == '--':
1561 if len(args) > 2:
1562 extra_args = args[2:]
1563 else:
1564 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001565
maruel380e3262016-08-31 16:10:06 -07001566 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001567 request = net.url_read_json(url)
1568 if not request:
1569 print >> sys.stderr, 'Failed to retrieve request data for the task'
1570 return 1
1571
maruel12e30012015-10-09 11:55:35 -07001572 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001573 if fs.isdir(workdir):
1574 parser.error('Please delete the directory \'work\' first')
1575 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001576 cachedir = unicode(os.path.abspath('cipd_cache'))
1577 if not fs.exists(cachedir):
1578 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001579
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001580 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001581 env = os.environ.copy()
1582 env['SWARMING_BOT_ID'] = 'reproduce'
1583 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001584 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001585 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001586 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001587 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001588 if not i['value']:
1589 env.pop(key, None)
1590 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001591 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001592
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001593 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001594 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001595 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001596 for i in env_prefixes:
1597 key = i['key']
1598 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001599 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
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001625 command = tools.fix_python_path(command)
1626 new_command = run_isolated.process_command(command, options.output_dir, None)
1627 if new_command != command:
1628 if not options.output_dir:
1629 parser.error('The task has outputs, you must use --output-dir')
1630 if not os.path.isdir(options.output_dir):
1631 os.makedir(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001632 command = new_command
1633 file_path.ensure_command_has_abs_path(command, workdir)
1634
1635 if properties.get('cipd_input'):
1636 ci = properties['cipd_input']
1637 cp = ci['client_package']
1638 client_manager = cipd.get_client(
1639 ci['server'], cp['package_name'], cp['version'], cachedir)
1640
1641 with client_manager as client:
1642 by_path = collections.defaultdict(list)
1643 for pkg in ci['packages']:
1644 path = pkg['path']
1645 # cipd deals with 'root' as ''
1646 if path == '.':
1647 path = ''
1648 by_path[path].append((pkg['package_name'], pkg['version']))
1649 client.ensure(workdir, by_path, cache_dir=cachedir)
1650
maruel77f720b2015-09-15 12:35:22 -07001651 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001652 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001653 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001654 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001655 print >> sys.stderr, str(e)
1656 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001657
1658
maruel0eb1d1b2015-10-02 14:48:21 -07001659@subcommand.usage('bot_id')
1660def CMDterminate(parser, args):
1661 """Tells a bot to gracefully shut itself down as soon as it can.
1662
1663 This is done by completing whatever current task there is then exiting the bot
1664 process.
1665 """
1666 parser.add_option(
1667 '--wait', action='store_true', help='Wait for the bot to terminate')
1668 options, args = parser.parse_args(args)
1669 if len(args) != 1:
1670 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001671 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001672 request = net.url_read_json(url, data={})
1673 if not request:
1674 print >> sys.stderr, 'Failed to ask for termination'
1675 return 1
1676 if options.wait:
1677 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001678 options.swarming,
1679 [request['task_id']],
1680 0.,
1681 False,
1682 False,
1683 None,
1684 None,
1685 [],
maruel9531ce02016-04-13 06:11:23 -07001686 False)
maruelbfc5f872017-06-10 16:43:17 -07001687 else:
1688 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001689 return 0
1690
1691
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001692@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001693def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001694 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001695
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001696 Passes all extra arguments provided after '--' as additional command line
1697 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001698 """
1699 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001700 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001701 parser.add_option(
1702 '--dump-json',
1703 metavar='FILE',
1704 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001705 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001706 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001707 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001708 tasks = trigger_task_shards(
1709 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001710 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001711 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001712 tasks_sorted = sorted(
1713 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001714 if options.dump_json:
1715 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001716 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001717 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001718 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001719 }
maruel46b015f2015-10-13 18:40:35 -07001720 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001721 print('To collect results, use:')
1722 print(' swarming.py collect -S %s --json %s' %
1723 (options.swarming, options.dump_json))
1724 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001725 print('To collect results, use:')
1726 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001727 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1728 print('Or visit:')
1729 for t in tasks_sorted:
1730 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001731 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001732 except Failure:
1733 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001734 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001735
1736
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001737class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001738 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001739 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001740 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001741 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001742 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001743 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001744 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001745 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001746 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001747 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001748
1749 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001750 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001751 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001752 auth.process_auth_options(self, options)
1753 user = self._process_swarming(options)
1754 if hasattr(options, 'user') and not options.user:
1755 options.user = user
1756 return options, args
1757
1758 def _process_swarming(self, options):
1759 """Processes the --swarming option and aborts if not specified.
1760
1761 Returns the identity as determined by the server.
1762 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001763 if not options.swarming:
1764 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001765 try:
1766 options.swarming = net.fix_url(options.swarming)
1767 except ValueError as e:
1768 self.error('--swarming %s' % e)
1769 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001770 try:
1771 user = auth.ensure_logged_in(options.swarming)
1772 except ValueError as e:
1773 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001774 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001775
1776
1777def main(args):
1778 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001779 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001780
1781
1782if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001783 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001784 fix_encoding.fix_encoding()
1785 tools.disable_buffering()
1786 colorama.init()
1787 sys.exit(main(sys.argv[1:]))