blob: 73627e856c18fad905f1c978d73c7bc3fe77a28a [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)
maruel77f720b2015-09-15 12:35:22 -0700584 result.setdefault('server_versions', None)
585 result.setdefault('started_ts', None)
586 result.setdefault('tags', None)
587 result.setdefault('user', None)
588
589 # Convertion back to old API.
590 duration = result.pop('duration', None)
591 result['durations'] = [duration] if duration else []
592 exit_code = result.pop('exit_code', None)
593 result['exit_codes'] = [int(exit_code)] if exit_code else []
594 result['id'] = result.pop('task_id')
595 result['isolated_out'] = result.get('outputs_ref', None)
596 output = result.pop('output', None)
597 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700598 # server_version
599 # Endpoints result 'state' as string. For compatibility with old code, convert
600 # to int.
601 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700602 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700603 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700604 if 'bot_dimensions' in result:
605 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700606 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700607 }
608 else:
609 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700610
611
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700612def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400613 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000614 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500615 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000616
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700617 Duplicate shards are ignored. Shards are yielded in order of completion.
618 Timed out shards are NOT yielded at all. Caller can compare number of yielded
619 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000620
621 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500622 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 +0000623 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500624
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700625 output_collector is an optional instance of TaskOutputCollector that will be
626 used to fetch files produced by a task from isolate server to the local disk.
627
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500628 Yields:
629 (index, result). In particular, 'result' is defined as the
630 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000631 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000632 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400633 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700634 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700635 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700636
maruel@chromium.org0437a732013-08-27 16:05:52 +0000637 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
638 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639 # Adds a task to the thread pool to call 'retrieve_results' and return
640 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700642 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000643 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400644 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000645 task_id, timeout, should_stop, output_collector, include_perf,
646 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700647
648 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400649 for shard_index, task_id in enumerate(task_ids):
650 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700651
652 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400653 shards_remaining = range(len(task_ids))
654 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700655 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700656 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700657 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700658 shard_index, result = results_channel.pull(
659 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 except threading_utils.TaskChannel.Timeout:
661 if print_status_updates:
662 print(
663 'Waiting for results from the following shards: %s' %
664 ', '.join(map(str, shards_remaining)))
665 sys.stdout.flush()
666 continue
667 except Exception:
668 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700669
670 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700671 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000672 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500673 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000674 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700675
Vadim Shtayurab450c602014-05-12 19:23:25 -0700676 # Yield back results to the caller.
677 assert shard_index in shards_remaining
678 shards_remaining.remove(shard_index)
679 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700680
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700682 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000683 should_stop.set()
684
685
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000686def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000687 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700688 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400689 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700690 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
691 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400692 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
693 metadata.get('abandoned_ts')):
694 pending = '%.1fs' % (
695 parse_time(metadata['abandoned_ts']) -
696 parse_time(metadata['created_ts'])
697 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400698 else:
699 pending = 'N/A'
700
maruel77f720b2015-09-15 12:35:22 -0700701 if metadata.get('duration') is not None:
702 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400703 else:
704 duration = 'N/A'
705
maruel77f720b2015-09-15 12:35:22 -0700706 if metadata.get('exit_code') is not None:
707 # Integers are encoded as string to not loose precision.
708 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400709 else:
710 exit_code = 'N/A'
711
712 bot_id = metadata.get('bot_id') or 'N/A'
713
maruel77f720b2015-09-15 12:35:22 -0700714 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400715 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000716 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400717 if metadata.get('state') == 'CANCELED':
718 tag_footer2 = ' Pending: %s CANCELED' % pending
719 elif metadata.get('state') == 'EXPIRED':
720 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
721 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT'):
722 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
723 pending, duration, bot_id, exit_code, metadata['state'])
724 else:
725 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
726 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400727
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000728 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
729 dash_pad = '+-%s-+' % ('-' * tag_len)
730 tag_header = '| %s |' % tag_header.ljust(tag_len)
731 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
732 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400733
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000734 if include_stdout:
735 return '\n'.join([
736 dash_pad,
737 tag_header,
738 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400739 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000740 dash_pad,
741 tag_footer1,
742 tag_footer2,
743 dash_pad,
744 ])
745 else:
746 return '\n'.join([
747 dash_pad,
748 tag_header,
749 tag_footer2,
750 dash_pad,
751 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000752
753
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700754def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700755 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000756 task_summary_json, task_output_dir, task_output_stdout,
757 include_perf):
maruela5490782015-09-30 10:56:59 -0700758 """Retrieves results of a Swarming task.
759
760 Returns:
761 process exit code that should be returned to the user.
762 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700763 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000764 output_collector = TaskOutputCollector(
765 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700766
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700767 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700768 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400769 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700770 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400771 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400772 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000773 output_collector, include_perf,
774 (len(task_output_stdout) > 0),
775 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700776 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700777
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400778 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700779 shard_exit_code = metadata.get('exit_code')
780 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700781 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700782 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700783 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400784 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700785 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700786
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700787 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000788 s = decorate_shard_output(
789 swarming, index, metadata,
790 "console" in task_output_stdout).encode(
791 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700792 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400793 if len(seen_shards) < len(task_ids):
794 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700795 else:
maruel77f720b2015-09-15 12:35:22 -0700796 print('%s: %s %s' % (
797 metadata.get('bot_id', 'N/A'),
798 metadata['task_id'],
799 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000800 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700801 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400802 if output:
803 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700804 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700805 summary = output_collector.finalize()
806 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700807 # TODO(maruel): Make this optional.
808 for i in summary['shards']:
809 if i:
810 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700811 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700812
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400813 if decorate and total_duration:
814 print('Total duration: %.1fs' % total_duration)
815
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400816 if len(seen_shards) != len(task_ids):
817 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700818 print >> sys.stderr, ('Results from some shards are missing: %s' %
819 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700820 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700821
maruela5490782015-09-30 10:56:59 -0700822 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000823
824
maruel77f720b2015-09-15 12:35:22 -0700825### API management.
826
827
828class APIError(Exception):
829 pass
830
831
832def endpoints_api_discovery_apis(host):
833 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
834 the APIs exposed by a host.
835
836 https://developers.google.com/discovery/v1/reference/apis/list
837 """
maruel380e3262016-08-31 16:10:06 -0700838 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
839 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700840 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
841 if data is None:
842 raise APIError('Failed to discover APIs on %s' % host)
843 out = {}
844 for api in data['items']:
845 if api['id'] == 'discovery:v1':
846 continue
847 # URL is of the following form:
848 # url = host + (
849 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
850 api_data = net.url_read_json(api['discoveryRestUrl'])
851 if api_data is None:
852 raise APIError('Failed to discover %s on %s' % (api['id'], host))
853 out[api['id']] = api_data
854 return out
855
856
maruelaf6b06c2017-06-08 06:26:53 -0700857def get_yielder(base_url, limit):
858 """Returns the first query and a function that yields following items."""
859 CHUNK_SIZE = 250
860
861 url = base_url
862 if limit:
863 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
864 data = net.url_read_json(url)
865 if data is None:
866 # TODO(maruel): Do basic diagnostic.
867 raise Failure('Failed to access %s' % url)
868 org_cursor = data.pop('cursor', None)
869 org_total = len(data.get('items') or [])
870 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
871 if not org_cursor or not org_total:
872 # This is not an iterable resource.
873 return data, lambda: []
874
875 def yielder():
876 cursor = org_cursor
877 total = org_total
878 # Some items support cursors. Try to get automatically if cursors are needed
879 # by looking at the 'cursor' items.
880 while cursor and (not limit or total < limit):
881 merge_char = '&' if '?' in base_url else '?'
882 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
883 if limit:
884 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
885 new = net.url_read_json(url)
886 if new is None:
887 raise Failure('Failed to access %s' % url)
888 cursor = new.get('cursor')
889 new_items = new.get('items')
890 nb_items = len(new_items or [])
891 total += nb_items
892 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
893 yield new_items
894
895 return data, yielder
896
897
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500898### Commands.
899
900
901def abort_task(_swarming, _manifest):
902 """Given a task manifest that was triggered, aborts its execution."""
903 # TODO(vadimsh): No supported by the server yet.
904
905
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400906def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800907 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500908 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500909 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500910 dest='dimensions', metavar='FOO bar',
911 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500912 parser.add_option_group(parser.filter_group)
913
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400914
maruelaf6b06c2017-06-08 06:26:53 -0700915def process_filter_options(parser, options):
916 for key, value in options.dimensions:
917 if ':' in key:
918 parser.error('--dimension key cannot contain ":"')
919 if key.strip() != key:
920 parser.error('--dimension key has whitespace')
921 if not key:
922 parser.error('--dimension key is empty')
923
924 if value.strip() != value:
925 parser.error('--dimension value has whitespace')
926 if not value:
927 parser.error('--dimension value is empty')
928 options.dimensions.sort()
929
930
Vadim Shtayurab450c602014-05-12 19:23:25 -0700931def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400932 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700933 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700934 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700935 help='Number of shards to trigger and collect.')
936 parser.add_option_group(parser.sharding_group)
937
938
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400939def add_trigger_options(parser):
940 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500941 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400942 add_filter_options(parser)
943
maruel681d6802017-01-17 16:56:03 -0800944 group = optparse.OptionGroup(parser, 'Task properties')
945 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700946 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500947 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800948 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500949 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700950 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800951 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800952 '--env-prefix', default=[], action='append', nargs=2,
953 metavar='VAR local/path',
954 help='Prepend task-relative `local/path` to the task\'s VAR environment '
955 'variable using os-appropriate pathsep character. Can be specified '
956 'multiple times for the same VAR to add multiple paths.')
957 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400958 '--idempotent', action='store_true', default=False,
959 help='When set, the server will actively try to find a previous task '
960 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800961 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700962 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700963 help='The optional path to a file containing the secret_bytes to use with'
964 'this task.')
maruel681d6802017-01-17 16:56:03 -0800965 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700966 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400967 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800968 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700969 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400970 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500972 '--raw-cmd', action='store_true', default=False,
973 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700974 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800975 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500976 '--relative-cwd',
977 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
978 'requires --raw-cmd')
979 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700980 '--cipd-package', action='append', default=[], metavar='PKG',
981 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700982 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800983 group.add_option(
984 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700985 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800986 help='"<name> <relpath>" items to keep a persistent bot managed cache')
987 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700988 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700989 help='Email of a service account to run the task as, or literal "bot" '
990 'string to indicate that the task should use the same account the '
991 'bot itself is using to authenticate to Swarming. Don\'t use task '
992 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800993 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700994 '-o', '--output', action='append', default=[], metavar='PATH',
995 help='A list of files to return in addition to those written to '
996 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
997 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -0800998 parser.add_option_group(group)
999
1000 group = optparse.OptionGroup(parser, 'Task request')
1001 group.add_option(
1002 '--priority', type='int', default=100,
1003 help='The lower value, the more important the task is')
1004 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001005 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001006 help='Display name of the task. Defaults to '
1007 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1008 'isolated file is provided, if a hash is provided, it defaults to '
1009 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1010 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001011 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001012 help='Tags to assign to the task.')
1013 group.add_option(
1014 '--user', default='',
1015 help='User associated with the task. Defaults to authenticated user on '
1016 'the server.')
1017 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001018 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001019 help='Seconds to allow the task to be pending for a bot to run before '
1020 'this task request expires.')
1021 group.add_option(
1022 '--deadline', type='int', dest='expiration',
1023 help=optparse.SUPPRESS_HELP)
1024 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001025
1026
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001027def process_trigger_options(parser, options, args):
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001028 """Processes trigger options and does preparatory steps."""
maruelaf6b06c2017-06-08 06:26:53 -07001029 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001030 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001031 if args and args[0] == '--':
1032 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001033
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001034 if not options.dimensions:
1035 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001036 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1037 parser.error('--tags must be in the format key:value')
1038 if options.raw_cmd and not args:
1039 parser.error(
1040 'Arguments with --raw-cmd should be passed after -- as command '
1041 'delimiter.')
1042 if options.isolate_server and not options.namespace:
1043 parser.error(
1044 '--namespace must be a valid value when --isolate-server is used')
1045 if not options.isolated and not options.raw_cmd:
1046 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1047
1048 # Isolated
1049 # --isolated is required only if --raw-cmd wasn't provided.
1050 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1051 # preferred server.
1052 isolateserver.process_isolate_server_options(
1053 parser, options, False, not options.raw_cmd)
1054 inputs_ref = None
1055 if options.isolate_server:
1056 inputs_ref = FilesRef(
1057 isolated=options.isolated,
1058 isolatedserver=options.isolate_server,
1059 namespace=options.namespace)
1060
1061 # Command
1062 command = None
1063 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001064 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001065 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001066 if options.relative_cwd:
1067 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1068 if not a.startswith(os.getcwd()):
1069 parser.error(
1070 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001071 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001072 if options.relative_cwd:
1073 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001074 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001075
maruel0a25f6c2017-05-10 10:43:23 -07001076 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001077 cipd_packages = []
1078 for p in options.cipd_package:
1079 split = p.split(':', 2)
1080 if len(split) != 3:
1081 parser.error('CIPD packages must take the form: path:package:version')
1082 cipd_packages.append(CipdPackage(
1083 package_name=split[1],
1084 path=split[0],
1085 version=split[2]))
1086 cipd_input = None
1087 if cipd_packages:
1088 cipd_input = CipdInput(
1089 client_package=None,
1090 packages=cipd_packages,
1091 server=None)
1092
maruel0a25f6c2017-05-10 10:43:23 -07001093 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001094 secret_bytes = None
1095 if options.secret_bytes_path:
1096 with open(options.secret_bytes_path, 'r') as f:
1097 secret_bytes = f.read().encode('base64')
1098
maruel0a25f6c2017-05-10 10:43:23 -07001099 # Named caches
maruel681d6802017-01-17 16:56:03 -08001100 caches = [
1101 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1102 for i in options.named_cache
1103 ]
maruel0a25f6c2017-05-10 10:43:23 -07001104
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001105 env_prefixes = {}
1106 for k, v in options.env_prefix:
1107 env_prefixes.setdefault(k, []).append(v)
1108
maruel77f720b2015-09-15 12:35:22 -07001109 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001110 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001111 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001112 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001113 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001114 dimensions=options.dimensions,
1115 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001116 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001117 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001118 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001119 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001120 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001121 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001122 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001123 outputs=options.output,
1124 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001125
maruel77f720b2015-09-15 12:35:22 -07001126 return NewTaskRequest(
1127 expiration_secs=options.expiration,
maruel0a25f6c2017-05-10 10:43:23 -07001128 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001129 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001130 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001131 properties=properties,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001132 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001133 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001134 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001135
1136
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001137class TaskOutputStdoutOption(optparse.Option):
1138 """Where to output the each task's console output (stderr/stdout).
1139
1140 The output will be;
1141 none - not be downloaded.
1142 json - stored in summary.json file *only*.
1143 console - shown on stdout *only*.
1144 all - stored in summary.json and shown on stdout.
1145 """
1146
1147 choices = ['all', 'json', 'console', 'none']
1148
1149 def __init__(self, *args, **kw):
1150 optparse.Option.__init__(
1151 self,
1152 *args,
1153 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001154 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001155 help=re.sub('\s\s*', ' ', self.__doc__),
1156 **kw)
1157
1158 def convert_value(self, opt, value):
1159 if value not in self.choices:
1160 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1161 self.get_opt_string(), self.choices, value))
1162 stdout_to = []
1163 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001164 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001165 elif value != 'none':
1166 stdout_to = [value]
1167 return stdout_to
1168
1169
maruel@chromium.org0437a732013-08-27 16:05:52 +00001170def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001171 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001172 '-t', '--timeout', type='float',
1173 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1174 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001175 parser.group_logging.add_option(
1176 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001177 parser.group_logging.add_option(
1178 '--print-status-updates', action='store_true',
1179 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001180 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001181 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001182 '--task-summary-json',
1183 metavar='FILE',
1184 help='Dump a summary of task results to this file as json. It contains '
1185 'only shards statuses as know to server directly. Any output files '
1186 'emitted by the task can be collected by using --task-output-dir')
1187 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001188 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001189 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001190 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001191 'directory contains per-shard directory with output files produced '
1192 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001193 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001194 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001195 parser.task_output_group.add_option(
1196 '--perf', action='store_true', default=False,
1197 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001198 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001199
1200
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001201@subcommand.usage('bots...')
1202def CMDbot_delete(parser, args):
1203 """Forcibly deletes bots from the Swarming server."""
1204 parser.add_option(
1205 '-f', '--force', action='store_true',
1206 help='Do not prompt for confirmation')
1207 options, args = parser.parse_args(args)
1208 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001209 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001210
1211 bots = sorted(args)
1212 if not options.force:
1213 print('Delete the following bots?')
1214 for bot in bots:
1215 print(' %s' % bot)
1216 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1217 print('Goodbye.')
1218 return 1
1219
1220 result = 0
1221 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001222 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001223 if net.url_read_json(url, data={}, method='POST') is None:
1224 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001225 result = 1
1226 return result
1227
1228
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001229def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001230 """Returns information about the bots connected to the Swarming server."""
1231 add_filter_options(parser)
1232 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001233 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001234 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001235 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001236 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001237 help='Keep both dead and alive bots')
1238 parser.filter_group.add_option(
1239 '--busy', action='store_true', help='Keep only busy bots')
1240 parser.filter_group.add_option(
1241 '--idle', action='store_true', help='Keep only idle bots')
1242 parser.filter_group.add_option(
1243 '--mp', action='store_true',
1244 help='Keep only Machine Provider managed bots')
1245 parser.filter_group.add_option(
1246 '--non-mp', action='store_true',
1247 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001248 parser.filter_group.add_option(
1249 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001250 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001251 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001252 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001253
1254 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001255 parser.error('Use only one of --keep-dead or --dead-only')
1256 if options.busy and options.idle:
1257 parser.error('Use only one of --busy or --idle')
1258 if options.mp and options.non_mp:
1259 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001260
maruelaf6b06c2017-06-08 06:26:53 -07001261 url = options.swarming + '/api/swarming/v1/bots/list?'
1262 values = []
1263 if options.dead_only:
1264 values.append(('is_dead', 'TRUE'))
1265 elif options.keep_dead:
1266 values.append(('is_dead', 'NONE'))
1267 else:
1268 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001269
maruelaf6b06c2017-06-08 06:26:53 -07001270 if options.busy:
1271 values.append(('is_busy', 'TRUE'))
1272 elif options.idle:
1273 values.append(('is_busy', 'FALSE'))
1274 else:
1275 values.append(('is_busy', 'NONE'))
1276
1277 if options.mp:
1278 values.append(('is_mp', 'TRUE'))
1279 elif options.non_mp:
1280 values.append(('is_mp', 'FALSE'))
1281 else:
1282 values.append(('is_mp', 'NONE'))
1283
1284 for key, value in options.dimensions:
1285 values.append(('dimensions', '%s:%s' % (key, value)))
1286 url += urllib.urlencode(values)
1287 try:
1288 data, yielder = get_yielder(url, 0)
1289 bots = data.get('items') or []
1290 for items in yielder():
1291 if items:
1292 bots.extend(items)
1293 except Failure as e:
1294 sys.stderr.write('\n%s\n' % e)
1295 return 1
maruel77f720b2015-09-15 12:35:22 -07001296 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001297 print bot['bot_id']
1298 if not options.bare:
1299 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1300 print ' %s' % json.dumps(dimensions, sort_keys=True)
1301 if bot.get('task_id'):
1302 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001303 return 0
1304
1305
maruelfd0a90c2016-06-10 11:51:10 -07001306@subcommand.usage('task_id')
1307def CMDcancel(parser, args):
1308 """Cancels a task."""
1309 options, args = parser.parse_args(args)
1310 if not args:
1311 parser.error('Please specify the task to cancel')
1312 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001313 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001314 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1315 print('Deleting %s failed. Probably already gone' % task_id)
1316 return 1
1317 return 0
1318
1319
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001320@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001321def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001322 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001323
1324 The result can be in multiple part if the execution was sharded. It can
1325 potentially have retries.
1326 """
1327 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001328 parser.add_option(
1329 '-j', '--json',
1330 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001331 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001332 if not args and not options.json:
1333 parser.error('Must specify at least one task id or --json.')
1334 if args and options.json:
1335 parser.error('Only use one of task id or --json.')
1336
1337 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001338 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001339 try:
maruel1ceb3872015-10-14 06:10:44 -07001340 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001341 data = json.load(f)
1342 except (IOError, ValueError):
1343 parser.error('Failed to open %s' % options.json)
1344 try:
1345 tasks = sorted(
1346 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1347 args = [t['task_id'] for t in tasks]
1348 except (KeyError, TypeError):
1349 parser.error('Failed to process %s' % options.json)
1350 if options.timeout is None:
1351 options.timeout = (
1352 data['request']['properties']['execution_timeout_secs'] +
1353 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001354 else:
1355 valid = frozenset('0123456789abcdef')
1356 if any(not valid.issuperset(task_id) for task_id in args):
1357 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001358
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001359 try:
1360 return collect(
1361 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001362 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001363 options.timeout,
1364 options.decorate,
1365 options.print_status_updates,
1366 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001367 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001368 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001369 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001370 except Failure:
1371 on_error.report(None)
1372 return 1
1373
1374
maruel77f720b2015-09-15 12:35:22 -07001375@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001376def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001377 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1378 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001379
1380 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001381 Raw task request and results:
1382 swarming.py query -S server-url.com task/123456/request
1383 swarming.py query -S server-url.com task/123456/result
1384
maruel77f720b2015-09-15 12:35:22 -07001385 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001386 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001387
maruelaf6b06c2017-06-08 06:26:53 -07001388 Listing last 10 tasks on a specific bot named 'bot1':
1389 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001390
maruelaf6b06c2017-06-08 06:26:53 -07001391 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001392 quoting is important!:
1393 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001394 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001395 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001396 parser.add_option(
1397 '-L', '--limit', type='int', default=200,
1398 help='Limit to enforce on limitless items (like number of tasks); '
1399 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001400 parser.add_option(
1401 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001402 parser.add_option(
1403 '--progress', action='store_true',
1404 help='Prints a dot at each request to show progress')
1405 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001406 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001407 parser.error(
1408 'Must specify only method name and optionally query args properly '
1409 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001410 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001411 try:
1412 data, yielder = get_yielder(base_url, options.limit)
1413 for items in yielder():
1414 if items:
1415 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001416 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001417 sys.stderr.write('.')
1418 sys.stderr.flush()
1419 except Failure as e:
1420 sys.stderr.write('\n%s\n' % e)
1421 return 1
maruel77f720b2015-09-15 12:35:22 -07001422 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001423 sys.stderr.write('\n')
1424 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001425 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001426 options.json = unicode(os.path.abspath(options.json))
1427 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001428 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001429 try:
maruel77f720b2015-09-15 12:35:22 -07001430 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001431 sys.stdout.write('\n')
1432 except IOError:
1433 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001434 return 0
1435
1436
maruel77f720b2015-09-15 12:35:22 -07001437def CMDquery_list(parser, args):
1438 """Returns list of all the Swarming APIs that can be used with command
1439 'query'.
1440 """
1441 parser.add_option(
1442 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1443 options, args = parser.parse_args(args)
1444 if args:
1445 parser.error('No argument allowed.')
1446
1447 try:
1448 apis = endpoints_api_discovery_apis(options.swarming)
1449 except APIError as e:
1450 parser.error(str(e))
1451 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001452 options.json = unicode(os.path.abspath(options.json))
1453 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001454 json.dump(apis, f)
1455 else:
1456 help_url = (
1457 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1458 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001459 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1460 if i:
1461 print('')
maruel77f720b2015-09-15 12:35:22 -07001462 print api_id
maruel11e31af2017-02-15 07:30:50 -08001463 print ' ' + api['description'].strip()
1464 if 'resources' in api:
1465 # Old.
1466 for j, (resource_name, resource) in enumerate(
1467 sorted(api['resources'].iteritems())):
1468 if j:
1469 print('')
1470 for method_name, method in sorted(resource['methods'].iteritems()):
1471 # Only list the GET ones.
1472 if method['httpMethod'] != 'GET':
1473 continue
1474 print '- %s.%s: %s' % (
1475 resource_name, method_name, method['path'])
1476 print('\n'.join(
1477 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1478 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1479 else:
1480 # New.
1481 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001482 # Only list the GET ones.
1483 if method['httpMethod'] != 'GET':
1484 continue
maruel11e31af2017-02-15 07:30:50 -08001485 print '- %s: %s' % (method['id'], method['path'])
1486 print('\n'.join(
1487 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001488 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1489 return 0
1490
1491
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001492@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001493def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001494 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001495
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001496 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001497 """
1498 add_trigger_options(parser)
1499 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001500 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001501 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001502 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001503 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001504 tasks = trigger_task_shards(
1505 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001506 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001507 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001508 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001509 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001510 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001511 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001512 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001513 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001514 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001515 task_ids = [
1516 t['task_id']
1517 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1518 ]
maruel71c61c82016-02-22 06:52:05 -08001519 if options.timeout is None:
1520 options.timeout = (
1521 task_request.properties.execution_timeout_secs +
1522 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001523 try:
1524 return collect(
1525 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001526 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001527 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001528 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001529 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001530 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001531 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001532 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001533 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001534 except Failure:
1535 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001536 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001537
1538
maruel18122c62015-10-23 06:31:23 -07001539@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001540def CMDreproduce(parser, args):
1541 """Runs a task locally that was triggered on the server.
1542
1543 This running locally the same commands that have been run on the bot. The data
1544 downloaded will be in a subdirectory named 'work' of the current working
1545 directory.
maruel18122c62015-10-23 06:31:23 -07001546
1547 You can pass further additional arguments to the target command by passing
1548 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001549 """
maruelc070e672016-02-22 17:32:57 -08001550 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001551 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001552 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001553 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001554 extra_args = []
1555 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001556 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001557 if len(args) > 1:
1558 if args[1] == '--':
1559 if len(args) > 2:
1560 extra_args = args[2:]
1561 else:
1562 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001563
maruel380e3262016-08-31 16:10:06 -07001564 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001565 request = net.url_read_json(url)
1566 if not request:
1567 print >> sys.stderr, 'Failed to retrieve request data for the task'
1568 return 1
1569
maruel12e30012015-10-09 11:55:35 -07001570 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001571 if fs.isdir(workdir):
1572 parser.error('Please delete the directory \'work\' first')
1573 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001574 cachedir = unicode(os.path.abspath('cipd_cache'))
1575 if not fs.exists(cachedir):
1576 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001577
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001578 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001579 env = os.environ.copy()
1580 env['SWARMING_BOT_ID'] = 'reproduce'
1581 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001582 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001583 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001584 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001585 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001586 if not i['value']:
1587 env.pop(key, None)
1588 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001589 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001590
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001591 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001592 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001593 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001594 for i in env_prefixes:
1595 key = i['key']
1596 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001597 cur = env.get(key)
1598 if cur:
1599 paths.append(cur)
1600 env[key] = os.path.pathsep.join(paths)
1601
iannucci31ab9192017-05-02 19:11:56 -07001602 command = []
nodir152cba62016-05-12 16:08:56 -07001603 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001604 # Create the tree.
1605 with isolateserver.get_storage(
1606 properties['inputs_ref']['isolatedserver'],
1607 properties['inputs_ref']['namespace']) as storage:
1608 bundle = isolateserver.fetch_isolated(
1609 properties['inputs_ref']['isolated'],
1610 storage,
1611 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001612 workdir,
1613 False)
maruel29ab2fd2015-10-16 11:44:01 -07001614 command = bundle.command
1615 if bundle.relative_cwd:
1616 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001617 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001618
1619 if properties.get('command'):
1620 command.extend(properties['command'])
1621
1622 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001623 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001624 if not options.output_dir:
1625 new_command = run_isolated.process_command(command, 'invalid', None)
1626 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001627 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001628 else:
1629 # Make the path absolute, as the process will run from a subdirectory.
1630 options.output_dir = os.path.abspath(options.output_dir)
1631 new_command = run_isolated.process_command(
1632 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001633 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001634 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001635 command = new_command
1636 file_path.ensure_command_has_abs_path(command, workdir)
1637
1638 if properties.get('cipd_input'):
1639 ci = properties['cipd_input']
1640 cp = ci['client_package']
1641 client_manager = cipd.get_client(
1642 ci['server'], cp['package_name'], cp['version'], cachedir)
1643
1644 with client_manager as client:
1645 by_path = collections.defaultdict(list)
1646 for pkg in ci['packages']:
1647 path = pkg['path']
1648 # cipd deals with 'root' as ''
1649 if path == '.':
1650 path = ''
1651 by_path[path].append((pkg['package_name'], pkg['version']))
1652 client.ensure(workdir, by_path, cache_dir=cachedir)
1653
maruel77f720b2015-09-15 12:35:22 -07001654 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001655 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001656 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001657 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001658 print >> sys.stderr, str(e)
1659 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001660
1661
maruel0eb1d1b2015-10-02 14:48:21 -07001662@subcommand.usage('bot_id')
1663def CMDterminate(parser, args):
1664 """Tells a bot to gracefully shut itself down as soon as it can.
1665
1666 This is done by completing whatever current task there is then exiting the bot
1667 process.
1668 """
1669 parser.add_option(
1670 '--wait', action='store_true', help='Wait for the bot to terminate')
1671 options, args = parser.parse_args(args)
1672 if len(args) != 1:
1673 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001674 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001675 request = net.url_read_json(url, data={})
1676 if not request:
1677 print >> sys.stderr, 'Failed to ask for termination'
1678 return 1
1679 if options.wait:
1680 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001681 options.swarming,
1682 [request['task_id']],
1683 0.,
1684 False,
1685 False,
1686 None,
1687 None,
1688 [],
maruel9531ce02016-04-13 06:11:23 -07001689 False)
maruelbfc5f872017-06-10 16:43:17 -07001690 else:
1691 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001692 return 0
1693
1694
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001695@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001696def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001697 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001698
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001699 Passes all extra arguments provided after '--' as additional command line
1700 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001701 """
1702 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001703 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001704 parser.add_option(
1705 '--dump-json',
1706 metavar='FILE',
1707 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001708 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001709 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001710 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001711 tasks = trigger_task_shards(
1712 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001713 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001714 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001715 tasks_sorted = sorted(
1716 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001717 if options.dump_json:
1718 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001719 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001720 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001721 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001722 }
maruel46b015f2015-10-13 18:40:35 -07001723 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001724 print('To collect results, use:')
1725 print(' swarming.py collect -S %s --json %s' %
1726 (options.swarming, options.dump_json))
1727 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001728 print('To collect results, use:')
1729 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001730 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1731 print('Or visit:')
1732 for t in tasks_sorted:
1733 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001734 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001735 except Failure:
1736 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001737 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001738
1739
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001740class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001741 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001742 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001743 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001744 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001745 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001746 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001747 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001748 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001749 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001750 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001751
1752 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001753 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001754 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001755 auth.process_auth_options(self, options)
1756 user = self._process_swarming(options)
1757 if hasattr(options, 'user') and not options.user:
1758 options.user = user
1759 return options, args
1760
1761 def _process_swarming(self, options):
1762 """Processes the --swarming option and aborts if not specified.
1763
1764 Returns the identity as determined by the server.
1765 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001766 if not options.swarming:
1767 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001768 try:
1769 options.swarming = net.fix_url(options.swarming)
1770 except ValueError as e:
1771 self.error('--swarming %s' % e)
1772 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001773 try:
1774 user = auth.ensure_logged_in(options.swarming)
1775 except ValueError as e:
1776 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001777 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001778
1779
1780def main(args):
1781 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001782 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001783
1784
1785if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001786 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001787 fix_encoding.fix_encoding()
1788 tools.disable_buffering()
1789 colorama.init()
1790 sys.exit(main(sys.argv[1:]))