blob: de9bbd449f7e937c1161994322cf946f78a2a2df [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2013 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# 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
maruel77f720b2015-09-15 12:35:22 -07008__version__ = '0.8.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
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import subprocess
17import sys
Vadim Shtayurab19319e2014-04-27 08:50:06 -070018import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000019import time
20import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000021
22from third_party import colorama
23from third_party.depot_tools import fix_encoding
24from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000025
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050026from utils import file_path
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040027from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040028from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000029from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040030from utils import on_error
maruel@chromium.org0437a732013-08-27 16:05:52 +000031from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000032from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000033
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080034import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040035import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000036import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000037
38
39ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050040
41
42class Failure(Exception):
43 """Generic failure."""
44 pass
45
46
47### Isolated file handling.
48
49
maruel77f720b2015-09-15 12:35:22 -070050def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050051 """Archives a .isolated file if needed.
52
53 Returns the file hash to trigger and a bool specifying if it was a file (True)
54 or a hash (False).
55 """
56 if arg.endswith('.isolated'):
maruel77f720b2015-09-15 12:35:22 -070057 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050058 if not file_hash:
59 on_error.report('Archival failure %s' % arg)
60 return None, True
61 return file_hash, True
62 elif isolated_format.is_valid_hash(arg, algo):
63 return arg, False
64 else:
65 on_error.report('Invalid hash %s' % arg)
66 return None, False
67
68
69def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050070 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050071
72 Returns:
maruel77f720b2015-09-15 12:35:22 -070073 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050074 """
75 isolated_cmd_args = []
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050076 if not options.isolated:
77 if '--' in args:
78 index = args.index('--')
79 isolated_cmd_args = args[index+1:]
80 args = args[:index]
81 else:
82 # optparse eats '--' sometimes.
83 isolated_cmd_args = args[1:]
84 args = args[:1]
85 if len(args) != 1:
86 raise ValueError(
87 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
88 'process.')
89 # Old code. To be removed eventually.
90 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070091 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050092 if not options.isolated:
93 raise ValueError('Invalid argument %s' % args[0])
94 elif args:
95 is_file = False
96 if '--' in args:
97 index = args.index('--')
98 isolated_cmd_args = args[index+1:]
99 if index != 0:
100 raise ValueError('Unexpected arguments.')
101 else:
102 # optparse eats '--' sometimes.
103 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500104
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500105 # If a file name was passed, use its base name of the isolated hash.
106 # Otherwise, use user name as an approximation of a task name.
107 if not options.task_name:
108 if is_file:
109 key = os.path.splitext(os.path.basename(args[0]))[0]
110 else:
111 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500112 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500113 key,
114 '_'.join(
115 '%s=%s' % (k, v)
116 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500117 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500118
maruel77f720b2015-09-15 12:35:22 -0700119 inputs_ref = FilesRef(
120 isolated=options.isolated,
121 isolatedserver=options.isolate_server,
122 namespace=options.namespace)
123 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500124
125
126### Triggering.
127
128
maruel77f720b2015-09-15 12:35:22 -0700129# See ../appengine/swarming/swarming_rpcs.py.
130FilesRef = collections.namedtuple(
131 'FilesRef',
132 [
133 'isolated',
134 'isolatedserver',
135 'namespace',
136 ])
137
138
139# See ../appengine/swarming/swarming_rpcs.py.
140TaskProperties = collections.namedtuple(
141 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500142 [
143 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144 'dimensions',
145 'env',
maruel77f720b2015-09-15 12:35:22 -0700146 'execution_timeout_secs',
147 'extra_args',
148 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500149 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700150 'inputs_ref',
151 'io_timeout_secs',
152 ])
153
154
155# See ../appengine/swarming/swarming_rpcs.py.
156NewTaskRequest = collections.namedtuple(
157 'NewTaskRequest',
158 [
159 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500160 'name',
maruel77f720b2015-09-15 12:35:22 -0700161 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500162 'priority',
maruel77f720b2015-09-15 12:35:22 -0700163 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 'tags',
165 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500166 ])
167
168
maruel77f720b2015-09-15 12:35:22 -0700169def namedtuple_to_dict(value):
170 """Recursively converts a namedtuple to a dict."""
171 out = dict(value._asdict())
172 for k, v in out.iteritems():
173 if hasattr(v, '_asdict'):
174 out[k] = namedtuple_to_dict(v)
175 return out
176
177
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500178def task_request_to_raw_request(task_request):
179 """Returns the json dict expected by the Swarming server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700180
181 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500182 """
maruel77f720b2015-09-15 12:35:22 -0700183 out = namedtuple_to_dict(task_request)
184 # Maps are not supported until protobuf v3.
185 out['properties']['dimensions'] = [
186 {'key': k, 'value': v}
187 for k, v in out['properties']['dimensions'].iteritems()
188 ]
189 out['properties']['dimensions'].sort(key=lambda x: x['key'])
190 out['properties']['env'] = [
191 {'key': k, 'value': v}
192 for k, v in out['properties']['env'].iteritems()
193 ]
194 out['properties']['env'].sort(key=lambda x: x['key'])
195 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500196
197
maruel77f720b2015-09-15 12:35:22 -0700198def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199 """Triggers a request on the Swarming server and returns the json data.
200
201 It's the low-level function.
202
203 Returns:
204 {
205 'request': {
206 'created_ts': u'2010-01-02 03:04:05',
207 'name': ..
208 },
209 'task_id': '12300',
210 }
211 """
212 logging.info('Triggering: %s', raw_request['name'])
213
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500214 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700215 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500216 if not result:
217 on_error.report('Failed to trigger task %s' % raw_request['name'])
218 return None
219 return result
220
221
222def setup_googletest(env, shards, index):
223 """Sets googletest specific environment variables."""
224 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700225 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
226 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
227 env = env[:]
228 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
229 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500230 return env
231
232
233def trigger_task_shards(swarming, task_request, shards):
234 """Triggers one or many subtasks of a sharded task.
235
236 Returns:
237 Dict with task details, returned to caller as part of --dump-json output.
238 None in case of failure.
239 """
240 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700241 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700243 req['properties']['env'] = setup_googletest(
244 req['properties']['env'], shards, index)
245 req['name'] += ':%s:%s' % (index, shards)
246 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500247
248 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500249 tasks = {}
250 priority_warning = False
251 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700252 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500253 if not task:
254 break
255 logging.info('Request result: %s', task)
256 if (not priority_warning and
257 task['request']['priority'] != task_request.priority):
258 priority_warning = True
259 print >> sys.stderr, (
260 'Priority was reset to %s' % task['request']['priority'])
261 tasks[request['name']] = {
262 'shard_index': index,
263 'task_id': task['task_id'],
264 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
265 }
266
267 # Some shards weren't triggered. Abort everything.
268 if len(tasks) != len(requests):
269 if tasks:
270 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
271 len(tasks), len(requests))
272 for task_dict in tasks.itervalues():
273 abort_task(swarming, task_dict['task_id'])
274 return None
275
276 return tasks
277
278
279### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000280
281
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700282# How often to print status updates to stdout in 'collect'.
283STATUS_UPDATE_INTERVAL = 15 * 60.
284
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400285
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400286class State(object):
287 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000288
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400289 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
290 values are part of the API so if they change, the API changed.
291
292 It's in fact an enum. Values should be in decreasing order of importance.
293 """
294 RUNNING = 0x10
295 PENDING = 0x20
296 EXPIRED = 0x30
297 TIMED_OUT = 0x40
298 BOT_DIED = 0x50
299 CANCELED = 0x60
300 COMPLETED = 0x70
301
maruel77f720b2015-09-15 12:35:22 -0700302 STATES = (
303 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
304 'COMPLETED')
305 STATES_RUNNING = ('RUNNING', 'PENDING')
306 STATES_NOT_RUNNING = (
307 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
308 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
309 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400310
311 _NAMES = {
312 RUNNING: 'Running',
313 PENDING: 'Pending',
314 EXPIRED: 'Expired',
315 TIMED_OUT: 'Execution timed out',
316 BOT_DIED: 'Bot died',
317 CANCELED: 'User canceled',
318 COMPLETED: 'Completed',
319 }
320
maruel77f720b2015-09-15 12:35:22 -0700321 _ENUMS = {
322 'RUNNING': RUNNING,
323 'PENDING': PENDING,
324 'EXPIRED': EXPIRED,
325 'TIMED_OUT': TIMED_OUT,
326 'BOT_DIED': BOT_DIED,
327 'CANCELED': CANCELED,
328 'COMPLETED': COMPLETED,
329 }
330
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400331 @classmethod
332 def to_string(cls, state):
333 """Returns a user-readable string representing a State."""
334 if state not in cls._NAMES:
335 raise ValueError('Invalid state %s' % state)
336 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000337
maruel77f720b2015-09-15 12:35:22 -0700338 @classmethod
339 def from_enum(cls, state):
340 """Returns int value based on the string."""
341 if state not in cls._ENUMS:
342 raise ValueError('Invalid state %s' % state)
343 return cls._ENUMS[state]
344
maruel@chromium.org0437a732013-08-27 16:05:52 +0000345
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700346class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700347 """Assembles task execution summary (for --task-summary-json output).
348
349 Optionally fetches task outputs from isolate server to local disk (used when
350 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700351
352 This object is shared among multiple threads running 'retrieve_results'
353 function, in particular they call 'process_shard_result' method in parallel.
354 """
355
maruel0eb1d1b2015-10-02 14:48:21 -0700356 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700357 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
358
359 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700360 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361 shard_count: expected number of task shards.
362 """
363 self.task_output_dir = task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364 self.shard_count = shard_count
365
366 self._lock = threading.Lock()
367 self._per_shard_results = {}
368 self._storage = None
369
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700370 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700371 os.makedirs(self.task_output_dir)
372
Vadim Shtayurab450c602014-05-12 19:23:25 -0700373 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374 """Stores results of a single task shard, fetches output files if necessary.
375
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400376 Modifies |result| in place.
377
maruel77f720b2015-09-15 12:35:22 -0700378 shard_index is 0-based.
379
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700380 Called concurrently from multiple threads.
381 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700383 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 if shard_index < 0 or shard_index >= self.shard_count:
385 logging.warning(
386 'Shard index %d is outside of expected range: [0; %d]',
387 shard_index, self.shard_count - 1)
388 return
389
maruel77f720b2015-09-15 12:35:22 -0700390 if result.get('outputs_ref'):
391 ref = result['outputs_ref']
392 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
393 ref['isolatedserver'],
394 urllib.urlencode(
395 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400396
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700397 # Store result dict of that shard, ignore results we've already seen.
398 with self._lock:
399 if shard_index in self._per_shard_results:
400 logging.warning('Ignoring duplicate shard index %d', shard_index)
401 return
402 self._per_shard_results[shard_index] = result
403
404 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700405 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400406 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700407 result['outputs_ref']['isolatedserver'],
408 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400409 if storage:
410 # Output files are supposed to be small and they are not reused across
411 # tasks. So use MemoryCache for them instead of on-disk cache. Make
412 # files writable, so that calling script can delete them.
413 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700414 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400415 storage,
416 isolateserver.MemoryCache(file_mode_mask=0700),
417 os.path.join(self.task_output_dir, str(shard_index)),
418 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700419
420 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700421 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700422 with self._lock:
423 # Write an array of shard results with None for missing shards.
424 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700425 'shards': [
426 self._per_shard_results.get(i) for i in xrange(self.shard_count)
427 ],
428 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700429 # Write summary.json to task_output_dir as well.
430 if self.task_output_dir:
431 tools.write_json(
432 os.path.join(self.task_output_dir, 'summary.json'),
433 summary,
434 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700435 if self._storage:
436 self._storage.close()
437 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700438 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439
440 def _get_storage(self, isolate_server, namespace):
441 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 with self._lock:
444 if not self._storage:
445 self._storage = isolateserver.get_storage(isolate_server, namespace)
446 else:
447 # Shards must all use exact same isolate server and namespace.
448 if self._storage.location != isolate_server:
449 logging.error(
450 'Task shards are using multiple isolate servers: %s and %s',
451 self._storage.location, isolate_server)
452 return None
453 if self._storage.namespace != namespace:
454 logging.error(
455 'Task shards are using multiple namespaces: %s and %s',
456 self._storage.namespace, namespace)
457 return None
458 return self._storage
459
460
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500461def now():
462 """Exists so it can be mocked easily."""
463 return time.time()
464
465
maruel77f720b2015-09-15 12:35:22 -0700466def parse_time(value):
467 """Converts serialized time from the API to datetime.datetime."""
468 # When microseconds are 0, the '.123456' suffix is elided. This means the
469 # serialized format is not consistent, which confuses the hell out of python.
470 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
471 try:
472 return datetime.datetime.strptime(value, fmt)
473 except ValueError:
474 pass
475 raise ValueError('Failed to parse %s' % value)
476
477
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700478def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400479 base_url, shard_index, task_id, timeout, should_stop, output_collector):
480 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700481
Vadim Shtayurab450c602014-05-12 19:23:25 -0700482 Returns:
483 <result dict> on success.
484 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700485 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000486 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700487 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
488 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700489 started = now()
490 deadline = started + timeout if timeout else None
491 attempt = 0
492
493 while not should_stop.is_set():
494 attempt += 1
495
496 # Waiting for too long -> give up.
497 current_time = now()
498 if deadline and current_time >= deadline:
499 logging.error('retrieve_results(%s) timed out on attempt %d',
500 base_url, attempt)
501 return None
502
503 # Do not spin too fast. Spin faster at the beginning though.
504 # Start with 1 sec delay and for each 30 sec of waiting add another second
505 # of delay, until hitting 15 sec ceiling.
506 if attempt > 1:
507 max_delay = min(15, 1 + (current_time - started) / 30.0)
508 delay = min(max_delay, deadline - current_time) if deadline else max_delay
509 if delay > 0:
510 logging.debug('Waiting %.1f sec before retrying', delay)
511 should_stop.wait(delay)
512 if should_stop.is_set():
513 return None
514
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400515 # Disable internal retries in net.url_read_json, since we are doing retries
516 # ourselves.
517 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700518 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
519 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400520 result = net.url_read_json(result_url, retry_50x=False)
521 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400522 continue
maruel77f720b2015-09-15 12:35:22 -0700523
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400524 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700525 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400526 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700527 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700528 # Record the result, try to fetch attached output files (if any).
529 if output_collector:
530 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700531 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700532 if result.get('internal_failure'):
533 logging.error('Internal error!')
534 elif result['state'] == 'BOT_DIED':
535 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700536 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000537
538
maruel77f720b2015-09-15 12:35:22 -0700539def convert_to_old_format(result):
540 """Converts the task result data from Endpoints API format to old API format
541 for compatibility.
542
543 This goes into the file generated as --task-summary-json.
544 """
545 # Sets default.
546 result.setdefault('abandoned_ts', None)
547 result.setdefault('bot_id', None)
548 result.setdefault('bot_version', None)
549 result.setdefault('children_task_ids', [])
550 result.setdefault('completed_ts', None)
551 result.setdefault('cost_saved_usd', None)
552 result.setdefault('costs_usd', None)
553 result.setdefault('deduped_from', None)
554 result.setdefault('name', None)
555 result.setdefault('outputs_ref', None)
556 result.setdefault('properties_hash', None)
557 result.setdefault('server_versions', None)
558 result.setdefault('started_ts', None)
559 result.setdefault('tags', None)
560 result.setdefault('user', None)
561
562 # Convertion back to old API.
563 duration = result.pop('duration', None)
564 result['durations'] = [duration] if duration else []
565 exit_code = result.pop('exit_code', None)
566 result['exit_codes'] = [int(exit_code)] if exit_code else []
567 result['id'] = result.pop('task_id')
568 result['isolated_out'] = result.get('outputs_ref', None)
569 output = result.pop('output', None)
570 result['outputs'] = [output] if output else []
571 # properties_hash
572 # server_version
573 # Endpoints result 'state' as string. For compatibility with old code, convert
574 # to int.
575 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700576 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700577 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700578 if 'bot_dimensions' in result:
579 result['bot_dimensions'] = {
580 i['key']: i['value'] for i in result['bot_dimensions']
581 }
582 else:
583 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700584
585
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700586def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400587 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
588 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500589 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000590
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700591 Duplicate shards are ignored. Shards are yielded in order of completion.
592 Timed out shards are NOT yielded at all. Caller can compare number of yielded
593 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000594
595 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500596 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 +0000597 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500598
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700599 output_collector is an optional instance of TaskOutputCollector that will be
600 used to fetch files produced by a task from isolate server to the local disk.
601
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500602 Yields:
603 (index, result). In particular, 'result' is defined as the
604 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000605 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000606 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400607 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700608 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700609 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700610
maruel@chromium.org0437a732013-08-27 16:05:52 +0000611 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
612 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700613 # Adds a task to the thread pool to call 'retrieve_results' and return
614 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400615 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700616 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400618 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
619 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700620
621 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400622 for shard_index, task_id in enumerate(task_ids):
623 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700624
625 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400626 shards_remaining = range(len(task_ids))
627 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700628 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700629 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700630 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700631 shard_index, result = results_channel.pull(
632 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700633 except threading_utils.TaskChannel.Timeout:
634 if print_status_updates:
635 print(
636 'Waiting for results from the following shards: %s' %
637 ', '.join(map(str, shards_remaining)))
638 sys.stdout.flush()
639 continue
640 except Exception:
641 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700642
643 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700644 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000645 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500646 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000647 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700648
Vadim Shtayurab450c602014-05-12 19:23:25 -0700649 # Yield back results to the caller.
650 assert shard_index in shards_remaining
651 shards_remaining.remove(shard_index)
652 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653
maruel@chromium.org0437a732013-08-27 16:05:52 +0000654 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700655 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656 should_stop.set()
657
658
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400659def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000660 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700661 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400662 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700663 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
664 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400665 else:
666 pending = 'N/A'
667
maruel77f720b2015-09-15 12:35:22 -0700668 if metadata.get('duration') is not None:
669 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400670 else:
671 duration = 'N/A'
672
maruel77f720b2015-09-15 12:35:22 -0700673 if metadata.get('exit_code') is not None:
674 # Integers are encoded as string to not loose precision.
675 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400676 else:
677 exit_code = 'N/A'
678
679 bot_id = metadata.get('bot_id') or 'N/A'
680
maruel77f720b2015-09-15 12:35:22 -0700681 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400682 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400683 tag_footer = (
684 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
685 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400686
687 tag_len = max(len(tag_header), len(tag_footer))
688 dash_pad = '+-%s-+\n' % ('-' * tag_len)
689 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
690 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
691
692 header = dash_pad + tag_header + dash_pad
693 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700694 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400695 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000696
697
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700698def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700699 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400700 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700701 """Retrieves results of a Swarming task.
702
703 Returns:
704 process exit code that should be returned to the user.
705 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700706 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700707 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700708
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700709 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700710 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400711 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700712 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400713 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400714 swarming, task_ids, timeout, None, print_status_updates,
715 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700716 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700717
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400718 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700719 shard_exit_code = metadata.get('exit_code')
720 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700721 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700722 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700723 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400724 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700725 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700726
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700727 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400728 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400729 if len(seen_shards) < len(task_ids):
730 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731 else:
maruel77f720b2015-09-15 12:35:22 -0700732 print('%s: %s %s' % (
733 metadata.get('bot_id', 'N/A'),
734 metadata['task_id'],
735 shard_exit_code))
736 if metadata['output']:
737 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400738 if output:
739 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700740 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700741 summary = output_collector.finalize()
742 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700743 # TODO(maruel): Make this optional.
744 for i in summary['shards']:
745 if i:
746 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700747 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700748
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400749 if decorate and total_duration:
750 print('Total duration: %.1fs' % total_duration)
751
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400752 if len(seen_shards) != len(task_ids):
753 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700754 print >> sys.stderr, ('Results from some shards are missing: %s' %
755 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700756 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700757
maruela5490782015-09-30 10:56:59 -0700758 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000759
760
maruel77f720b2015-09-15 12:35:22 -0700761### API management.
762
763
764class APIError(Exception):
765 pass
766
767
768def endpoints_api_discovery_apis(host):
769 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
770 the APIs exposed by a host.
771
772 https://developers.google.com/discovery/v1/reference/apis/list
773 """
774 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
775 if data is None:
776 raise APIError('Failed to discover APIs on %s' % host)
777 out = {}
778 for api in data['items']:
779 if api['id'] == 'discovery:v1':
780 continue
781 # URL is of the following form:
782 # url = host + (
783 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
784 api_data = net.url_read_json(api['discoveryRestUrl'])
785 if api_data is None:
786 raise APIError('Failed to discover %s on %s' % (api['id'], host))
787 out[api['id']] = api_data
788 return out
789
790
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500791### Commands.
792
793
794def abort_task(_swarming, _manifest):
795 """Given a task manifest that was triggered, aborts its execution."""
796 # TODO(vadimsh): No supported by the server yet.
797
798
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400799def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400800 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500801 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500802 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500803 dest='dimensions', metavar='FOO bar',
804 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500805 parser.add_option_group(parser.filter_group)
806
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400807
Vadim Shtayurab450c602014-05-12 19:23:25 -0700808def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400809 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700810 parser.sharding_group.add_option(
811 '--shards', type='int', default=1,
812 help='Number of shards to trigger and collect.')
813 parser.add_option_group(parser.sharding_group)
814
815
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400816def add_trigger_options(parser):
817 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500818 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400819 add_filter_options(parser)
820
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400821 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500822 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500823 '-s', '--isolated',
824 help='Hash of the .isolated to grab from the isolate server')
825 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500826 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700827 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500828 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500829 '--priority', type='int', default=100,
830 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500831 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500832 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400833 help='Display name of the task. Defaults to '
834 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
835 'isolated file is provided, if a hash is provided, it defaults to '
836 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400837 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400838 '--tags', action='append', default=[],
839 help='Tags to assign to the task.')
840 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500841 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400842 help='User associated with the task. Defaults to authenticated user on '
843 'the server.')
844 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400845 '--idempotent', action='store_true', default=False,
846 help='When set, the server will actively try to find a previous task '
847 'with the same parameter and return this result instead if possible')
848 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400849 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400850 help='Seconds to allow the task to be pending for a bot to run before '
851 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400852 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400853 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400854 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400855 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400856 '--hard-timeout', type='int', default=60*60,
857 help='Seconds to allow the task to complete.')
858 parser.task_group.add_option(
859 '--io-timeout', type='int', default=20*60,
860 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500861 parser.task_group.add_option(
862 '--raw-cmd', action='store_true', default=False,
863 help='When set, the command after -- is used as-is without run_isolated. '
864 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500865 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000866
867
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500868def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500869 """Processes trigger options and uploads files to isolate server if necessary.
870 """
871 options.dimensions = dict(options.dimensions)
872 options.env = dict(options.env)
873
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500874 if not options.dimensions:
875 parser.error('Please at least specify one --dimension')
876 if options.raw_cmd:
877 if not args:
878 parser.error(
879 'Arguments with --raw-cmd should be passed after -- as command '
880 'delimiter.')
881 if options.isolate_server:
882 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
883
884 command = args
885 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500886 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500887 options.user,
888 '_'.join(
889 '%s=%s' % (k, v)
890 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700891 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500892 else:
893 isolateserver.process_isolate_server_options(parser, options, False)
894 try:
maruel77f720b2015-09-15 12:35:22 -0700895 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500896 except ValueError as e:
897 parser.error(str(e))
898
maruel77f720b2015-09-15 12:35:22 -0700899 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
900 # actual command to run.
901 properties = TaskProperties(
902 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500903 dimensions=options.dimensions,
904 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700905 execution_timeout_secs=options.hard_timeout,
906 extra_args=command if inputs_ref else None,
907 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500908 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700909 inputs_ref=inputs_ref,
910 io_timeout_secs=options.io_timeout)
911 return NewTaskRequest(
912 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500913 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700914 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500915 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700916 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500917 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700918 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000919
920
921def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500922 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000923 '-t', '--timeout',
924 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400925 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000926 help='Timeout to wait for result, set to 0 for no timeout; default: '
927 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500928 parser.group_logging.add_option(
929 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700930 parser.group_logging.add_option(
931 '--print-status-updates', action='store_true',
932 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400933 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700934 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700935 '--task-summary-json',
936 metavar='FILE',
937 help='Dump a summary of task results to this file as json. It contains '
938 'only shards statuses as know to server directly. Any output files '
939 'emitted by the task can be collected by using --task-output-dir')
940 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700941 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700942 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700943 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700944 'directory contains per-shard directory with output files produced '
945 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700946 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000947
948
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400949@subcommand.usage('bots...')
950def CMDbot_delete(parser, args):
951 """Forcibly deletes bots from the Swarming server."""
952 parser.add_option(
953 '-f', '--force', action='store_true',
954 help='Do not prompt for confirmation')
955 options, args = parser.parse_args(args)
956 if not args:
957 parser.error('Please specific bots to delete')
958
959 bots = sorted(args)
960 if not options.force:
961 print('Delete the following bots?')
962 for bot in bots:
963 print(' %s' % bot)
964 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
965 print('Goodbye.')
966 return 1
967
968 result = 0
969 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -0700970 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
971 if net.url_read_json(url, data={}, method='POST') is None:
972 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400973 result = 1
974 return result
975
976
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400977def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400978 """Returns information about the bots connected to the Swarming server."""
979 add_filter_options(parser)
980 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400981 '--dead-only', action='store_true',
982 help='Only print dead bots, useful to reap them and reimage broken bots')
983 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400984 '-k', '--keep-dead', action='store_true',
985 help='Do not filter out dead bots')
986 parser.filter_group.add_option(
987 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400988 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400989 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400990
991 if options.keep_dead and options.dead_only:
992 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700993
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400994 bots = []
995 cursor = None
996 limit = 250
997 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -0700998 base_url = (
999 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001000 while True:
1001 url = base_url
1002 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001003 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001004 data = net.url_read_json(url)
1005 if data is None:
1006 print >> sys.stderr, 'Failed to access %s' % options.swarming
1007 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001008 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001009 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001010 if not cursor:
1011 break
1012
maruel77f720b2015-09-15 12:35:22 -07001013 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001014 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001015 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001016 continue
maruel77f720b2015-09-15 12:35:22 -07001017 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001018 continue
1019
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001020 # If the user requested to filter on dimensions, ensure the bot has all the
1021 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001022 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001023 for key, value in options.dimensions:
1024 if key not in dimensions:
1025 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001026 # A bot can have multiple value for a key, for example,
1027 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1028 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001029 if isinstance(dimensions[key], list):
1030 if value not in dimensions[key]:
1031 break
1032 else:
1033 if value != dimensions[key]:
1034 break
1035 else:
maruel77f720b2015-09-15 12:35:22 -07001036 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001037 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001038 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001039 if bot.get('task_id'):
1040 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001041 return 0
1042
1043
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001044@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001045def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001046 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001047
1048 The result can be in multiple part if the execution was sharded. It can
1049 potentially have retries.
1050 """
1051 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001052 parser.add_option(
1053 '-j', '--json',
1054 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001055 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001056 if not args and not options.json:
1057 parser.error('Must specify at least one task id or --json.')
1058 if args and options.json:
1059 parser.error('Only use one of task id or --json.')
1060
1061 if options.json:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001062 try:
1063 with open(options.json) as f:
1064 tasks = sorted(
1065 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1066 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001067 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001068 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001069 else:
1070 valid = frozenset('0123456789abcdef')
1071 if any(not valid.issuperset(task_id) for task_id in args):
1072 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001073
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001074 try:
1075 return collect(
1076 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001077 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001078 options.timeout,
1079 options.decorate,
1080 options.print_status_updates,
1081 options.task_summary_json,
1082 options.task_output_dir)
1083 except Failure:
1084 on_error.report(None)
1085 return 1
1086
1087
maruelbea00862015-09-18 09:55:36 -07001088@subcommand.usage('[filename]')
1089def CMDput_bootstrap(parser, args):
1090 """Uploads a new version of bootstrap.py."""
1091 options, args = parser.parse_args(args)
1092 if len(args) != 1:
1093 parser.error('Must specify file to upload')
1094 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
1095 with open(args[0], 'rb') as f:
1096 content = f.read().decode('utf-8')
1097 data = net.url_read_json(url, data={'content': content})
1098 print data
1099 return 0
1100
1101
1102@subcommand.usage('[filename]')
1103def CMDput_bot_config(parser, args):
1104 """Uploads a new version of bot_config.py."""
1105 options, args = parser.parse_args(args)
1106 if len(args) != 1:
1107 parser.error('Must specify file to upload')
1108 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
1109 with open(args[0], 'rb') as f:
1110 content = f.read().decode('utf-8')
1111 data = net.url_read_json(url, data={'content': content})
1112 print data
1113 return 0
1114
1115
maruel77f720b2015-09-15 12:35:22 -07001116@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001117def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001118 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1119 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001120
1121 Examples:
maruel77f720b2015-09-15 12:35:22 -07001122 Listing all bots:
1123 swarming.py query -S https://server-url bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001124
maruel77f720b2015-09-15 12:35:22 -07001125 Listing last 10 tasks on a specific bot named 'swarm1':
1126 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001127 """
1128 CHUNK_SIZE = 250
1129
1130 parser.add_option(
1131 '-L', '--limit', type='int', default=200,
1132 help='Limit to enforce on limitless items (like number of tasks); '
1133 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001134 parser.add_option(
1135 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001136 parser.add_option(
1137 '--progress', action='store_true',
1138 help='Prints a dot at each request to show progress')
1139 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001140 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001141 parser.error(
1142 'Must specify only method name and optionally query args properly '
1143 'escaped.')
1144 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001145 url = base_url
1146 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001147 # Check check, change if not working out.
1148 merge_char = '&' if '?' in url else '?'
1149 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001150 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001151 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001152 # TODO(maruel): Do basic diagnostic.
1153 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001154 return 1
1155
1156 # Some items support cursors. Try to get automatically if cursors are needed
1157 # by looking at the 'cursor' items.
1158 while (
1159 data.get('cursor') and
1160 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001161 merge_char = '&' if '?' in base_url else '?'
1162 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001163 if options.limit:
1164 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001165 if options.progress:
1166 sys.stdout.write('.')
1167 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001168 new = net.url_read_json(url)
1169 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001170 if options.progress:
1171 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001172 print >> sys.stderr, 'Failed to access %s' % options.swarming
1173 return 1
1174 data['items'].extend(new['items'])
maruel77f720b2015-09-15 12:35:22 -07001175 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001176
maruel77f720b2015-09-15 12:35:22 -07001177 if options.progress:
1178 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001179 if options.limit and len(data.get('items', [])) > options.limit:
1180 data['items'] = data['items'][:options.limit]
1181 data.pop('cursor', None)
1182
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001183 if options.json:
maruel77f720b2015-09-15 12:35:22 -07001184 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001185 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001186 try:
maruel77f720b2015-09-15 12:35:22 -07001187 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001188 sys.stdout.write('\n')
1189 except IOError:
1190 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001191 return 0
1192
1193
maruel77f720b2015-09-15 12:35:22 -07001194def CMDquery_list(parser, args):
1195 """Returns list of all the Swarming APIs that can be used with command
1196 'query'.
1197 """
1198 parser.add_option(
1199 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1200 options, args = parser.parse_args(args)
1201 if args:
1202 parser.error('No argument allowed.')
1203
1204 try:
1205 apis = endpoints_api_discovery_apis(options.swarming)
1206 except APIError as e:
1207 parser.error(str(e))
1208 if options.json:
1209 with open(options.json, 'wb') as f:
1210 json.dump(apis, f)
1211 else:
1212 help_url = (
1213 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1214 options.swarming)
1215 for api_id, api in sorted(apis.iteritems()):
1216 print api_id
1217 print ' ' + api['description']
1218 for resource_name, resource in sorted(api['resources'].iteritems()):
1219 print ''
1220 for method_name, method in sorted(resource['methods'].iteritems()):
1221 # Only list the GET ones.
1222 if method['httpMethod'] != 'GET':
1223 continue
1224 print '- %s.%s: %s' % (
1225 resource_name, method_name, method['path'])
1226 print ' ' + method['description']
1227 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1228 return 0
1229
1230
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001231@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001232def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001233 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001234
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001235 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001236 """
1237 add_trigger_options(parser)
1238 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001239 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001240 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001241 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001242 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001243 tasks = trigger_task_shards(
1244 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001245 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001246 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001247 'Failed to trigger %s(%s): %s' %
1248 (options.task_name, args[0], e.args[0]))
1249 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001250 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001251 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001252 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001253 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001254 task_ids = [
1255 t['task_id']
1256 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1257 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001258 try:
1259 return collect(
1260 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001261 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001262 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001263 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001264 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001265 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001266 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001267 except Failure:
1268 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001269 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001270
1271
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001272@subcommand.usage('task_id')
1273def CMDreproduce(parser, args):
1274 """Runs a task locally that was triggered on the server.
1275
1276 This running locally the same commands that have been run on the bot. The data
1277 downloaded will be in a subdirectory named 'work' of the current working
1278 directory.
1279 """
1280 options, args = parser.parse_args(args)
1281 if len(args) != 1:
1282 parser.error('Must specify exactly one task id.')
1283
maruel77f720b2015-09-15 12:35:22 -07001284 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001285 request = net.url_read_json(url)
1286 if not request:
1287 print >> sys.stderr, 'Failed to retrieve request data for the task'
1288 return 1
1289
1290 if not os.path.isdir('work'):
1291 os.mkdir('work')
1292
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001293 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001294 env = None
1295 if properties['env']:
1296 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001297 logging.info('env: %r', properties['env'])
1298 env.update(
maruel77f720b2015-09-15 12:35:22 -07001299 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1300 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001301
maruel77f720b2015-09-15 12:35:22 -07001302 try:
1303 return subprocess.call(properties['command'], env=env, cwd='work')
1304 except OSError as e:
1305 print >> sys.stderr, 'Failed to run: %s' % ' '.join(properties['command'])
1306 print >> sys.stderr, str(e)
1307 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001308
1309
maruel0eb1d1b2015-10-02 14:48:21 -07001310@subcommand.usage('bot_id')
1311def CMDterminate(parser, args):
1312 """Tells a bot to gracefully shut itself down as soon as it can.
1313
1314 This is done by completing whatever current task there is then exiting the bot
1315 process.
1316 """
1317 parser.add_option(
1318 '--wait', action='store_true', help='Wait for the bot to terminate')
1319 options, args = parser.parse_args(args)
1320 if len(args) != 1:
1321 parser.error('Please provide the bot id')
1322 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1323 request = net.url_read_json(url, data={})
1324 if not request:
1325 print >> sys.stderr, 'Failed to ask for termination'
1326 return 1
1327 if options.wait:
1328 return collect(
1329 options.swarming, [request['task_id']], 0., False, False, None, None)
1330 return 0
1331
1332
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001333@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001334def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001335 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001336
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001337 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001338 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001339
1340 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001341
1342 Passes all extra arguments provided after '--' as additional command line
1343 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001344 """
1345 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001346 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001347 parser.add_option(
1348 '--dump-json',
1349 metavar='FILE',
1350 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001351 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001352 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001353 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001354 tasks = trigger_task_shards(
1355 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001356 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001357 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001358 tasks_sorted = sorted(
1359 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001360 if options.dump_json:
1361 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001362 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001363 'tasks': tasks,
1364 }
1365 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001366 print('To collect results, use:')
1367 print(' swarming.py collect -S %s --json %s' %
1368 (options.swarming, options.dump_json))
1369 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001370 print('To collect results, use:')
1371 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001372 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1373 print('Or visit:')
1374 for t in tasks_sorted:
1375 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001376 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001377 except Failure:
1378 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001379 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001380
1381
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001382class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001383 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001384 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001385 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001386 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001387 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001388 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001389 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001390 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001391 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001392 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001393
1394 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001395 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001396 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001397 auth.process_auth_options(self, options)
1398 user = self._process_swarming(options)
1399 if hasattr(options, 'user') and not options.user:
1400 options.user = user
1401 return options, args
1402
1403 def _process_swarming(self, options):
1404 """Processes the --swarming option and aborts if not specified.
1405
1406 Returns the identity as determined by the server.
1407 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001408 if not options.swarming:
1409 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001410 try:
1411 options.swarming = net.fix_url(options.swarming)
1412 except ValueError as e:
1413 self.error('--swarming %s' % e)
1414 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001415 try:
1416 user = auth.ensure_logged_in(options.swarming)
1417 except ValueError as e:
1418 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001419 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001420
1421
1422def main(args):
1423 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001424 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001425
1426
1427if __name__ == '__main__':
1428 fix_encoding.fix_encoding()
1429 tools.disable_buffering()
1430 colorama.init()
1431 sys.exit(main(sys.argv[1:]))