blob: 1a258a59e567b143c01489cab4c836cf188fb828 [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
356 def __init__(self, task_output_dir, task_name, shard_count):
357 """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 task_name: name of the swarming task results belong to.
362 shard_count: expected number of task shards.
363 """
364 self.task_output_dir = task_output_dir
365 self.task_name = task_name
366 self.shard_count = shard_count
367
368 self._lock = threading.Lock()
369 self._per_shard_results = {}
370 self._storage = None
371
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700372 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700373 os.makedirs(self.task_output_dir)
374
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),
419 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 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700431 # Write summary.json to task_output_dir as well.
432 if self.task_output_dir:
433 tools.write_json(
434 os.path.join(self.task_output_dir, 'summary.json'),
435 summary,
436 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700437 if self._storage:
438 self._storage.close()
439 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441
442 def _get_storage(self, isolate_server, namespace):
443 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700444 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 with self._lock:
446 if not self._storage:
447 self._storage = isolateserver.get_storage(isolate_server, namespace)
448 else:
449 # Shards must all use exact same isolate server and namespace.
450 if self._storage.location != isolate_server:
451 logging.error(
452 'Task shards are using multiple isolate servers: %s and %s',
453 self._storage.location, isolate_server)
454 return None
455 if self._storage.namespace != namespace:
456 logging.error(
457 'Task shards are using multiple namespaces: %s and %s',
458 self._storage.namespace, namespace)
459 return None
460 return self._storage
461
462
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500463def now():
464 """Exists so it can be mocked easily."""
465 return time.time()
466
467
maruel77f720b2015-09-15 12:35:22 -0700468def parse_time(value):
469 """Converts serialized time from the API to datetime.datetime."""
470 # When microseconds are 0, the '.123456' suffix is elided. This means the
471 # serialized format is not consistent, which confuses the hell out of python.
472 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
473 try:
474 return datetime.datetime.strptime(value, fmt)
475 except ValueError:
476 pass
477 raise ValueError('Failed to parse %s' % value)
478
479
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700480def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400481 base_url, shard_index, task_id, timeout, should_stop, output_collector):
482 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700483
Vadim Shtayurab450c602014-05-12 19:23:25 -0700484 Returns:
485 <result dict> on success.
486 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700487 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000488 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700489 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
490 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700491 started = now()
492 deadline = started + timeout if timeout else None
493 attempt = 0
494
495 while not should_stop.is_set():
496 attempt += 1
497
498 # Waiting for too long -> give up.
499 current_time = now()
500 if deadline and current_time >= deadline:
501 logging.error('retrieve_results(%s) timed out on attempt %d',
502 base_url, attempt)
503 return None
504
505 # Do not spin too fast. Spin faster at the beginning though.
506 # Start with 1 sec delay and for each 30 sec of waiting add another second
507 # of delay, until hitting 15 sec ceiling.
508 if attempt > 1:
509 max_delay = min(15, 1 + (current_time - started) / 30.0)
510 delay = min(max_delay, deadline - current_time) if deadline else max_delay
511 if delay > 0:
512 logging.debug('Waiting %.1f sec before retrying', delay)
513 should_stop.wait(delay)
514 if should_stop.is_set():
515 return None
516
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400517 # Disable internal retries in net.url_read_json, since we are doing retries
518 # ourselves.
519 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
520 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
528 if not result['output']:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400529 logging.error('No output found for task %s', task_id)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700530 # Record the result, try to fetch attached output files (if any).
531 if output_collector:
532 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700533 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700534 if result.get('internal_failure'):
535 logging.error('Internal error!')
536 elif result['state'] == 'BOT_DIED':
537 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700538 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000539
540
maruel77f720b2015-09-15 12:35:22 -0700541def convert_to_old_format(result):
542 """Converts the task result data from Endpoints API format to old API format
543 for compatibility.
544
545 This goes into the file generated as --task-summary-json.
546 """
547 # Sets default.
548 result.setdefault('abandoned_ts', None)
549 result.setdefault('bot_id', None)
550 result.setdefault('bot_version', None)
551 result.setdefault('children_task_ids', [])
552 result.setdefault('completed_ts', None)
553 result.setdefault('cost_saved_usd', None)
554 result.setdefault('costs_usd', None)
555 result.setdefault('deduped_from', None)
556 result.setdefault('name', None)
557 result.setdefault('outputs_ref', None)
558 result.setdefault('properties_hash', None)
559 result.setdefault('server_versions', None)
560 result.setdefault('started_ts', None)
561 result.setdefault('tags', None)
562 result.setdefault('user', None)
563
564 # Convertion back to old API.
565 duration = result.pop('duration', None)
566 result['durations'] = [duration] if duration else []
567 exit_code = result.pop('exit_code', None)
568 result['exit_codes'] = [int(exit_code)] if exit_code else []
569 result['id'] = result.pop('task_id')
570 result['isolated_out'] = result.get('outputs_ref', None)
571 output = result.pop('output', None)
572 result['outputs'] = [output] if output else []
573 # properties_hash
574 # server_version
575 # Endpoints result 'state' as string. For compatibility with old code, convert
576 # to int.
577 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700578 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700579 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700580 if 'bot_dimensions' in result:
581 result['bot_dimensions'] = {
582 i['key']: i['value'] for i in result['bot_dimensions']
583 }
584 else:
585 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700586
587
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700588def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400589 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
590 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500591 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000592
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700593 Duplicate shards are ignored. Shards are yielded in order of completion.
594 Timed out shards are NOT yielded at all. Caller can compare number of yielded
595 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000596
597 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500598 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 +0000599 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500600
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700601 output_collector is an optional instance of TaskOutputCollector that will be
602 used to fetch files produced by a task from isolate server to the local disk.
603
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500604 Yields:
605 (index, result). In particular, 'result' is defined as the
606 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000607 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000608 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400609 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700610 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700611 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700612
maruel@chromium.org0437a732013-08-27 16:05:52 +0000613 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
614 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700615 # Adds a task to the thread pool to call 'retrieve_results' and return
616 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400617 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700618 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000619 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400620 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
621 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700622
623 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400624 for shard_index, task_id in enumerate(task_ids):
625 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700626
627 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400628 shards_remaining = range(len(task_ids))
629 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700630 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700631 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700632 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 shard_index, result = results_channel.pull(
634 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700635 except threading_utils.TaskChannel.Timeout:
636 if print_status_updates:
637 print(
638 'Waiting for results from the following shards: %s' %
639 ', '.join(map(str, shards_remaining)))
640 sys.stdout.flush()
641 continue
642 except Exception:
643 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700644
645 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700646 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000647 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500648 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000649 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700650
Vadim Shtayurab450c602014-05-12 19:23:25 -0700651 # Yield back results to the caller.
652 assert shard_index in shards_remaining
653 shards_remaining.remove(shard_index)
654 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700655
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700657 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000658 should_stop.set()
659
660
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400661def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000662 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700663 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400664 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700665 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
666 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400667 else:
668 pending = 'N/A'
669
maruel77f720b2015-09-15 12:35:22 -0700670 if metadata.get('duration') is not None:
671 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400672 else:
673 duration = 'N/A'
674
maruel77f720b2015-09-15 12:35:22 -0700675 if metadata.get('exit_code') is not None:
676 # Integers are encoded as string to not loose precision.
677 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400678 else:
679 exit_code = 'N/A'
680
681 bot_id = metadata.get('bot_id') or 'N/A'
682
maruel77f720b2015-09-15 12:35:22 -0700683 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400684 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400685 tag_footer = (
686 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
687 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400688
689 tag_len = max(len(tag_header), len(tag_footer))
690 dash_pad = '+-%s-+\n' % ('-' * tag_len)
691 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
692 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
693
694 header = dash_pad + tag_header + dash_pad
695 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700696 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400697 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000698
699
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700700def collect(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400701 swarming, task_name, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400702 task_summary_json, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500703 """Retrieves results of a Swarming task."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700704 # Collect summary JSON and output files (if task_output_dir is not None).
705 output_collector = TaskOutputCollector(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400706 task_output_dir, task_name, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700707
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700708 seen_shards = set()
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400709 exit_code = 0
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400710 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700711 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400712 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400713 swarming, task_ids, timeout, None, print_status_updates,
714 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700715 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700716
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400717 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700718 shard_exit_code = metadata.get('exit_code')
719 if shard_exit_code:
720 shard_exit_code = int(shard_exit_code)
maruel8db72b72015-09-02 13:28:11 -0700721 if shard_exit_code:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400722 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700723 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700724
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700725 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400726 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400727 if len(seen_shards) < len(task_ids):
728 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700729 else:
maruel77f720b2015-09-15 12:35:22 -0700730 print('%s: %s %s' % (
731 metadata.get('bot_id', 'N/A'),
732 metadata['task_id'],
733 shard_exit_code))
734 if metadata['output']:
735 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400736 if output:
737 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700738 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700739 summary = output_collector.finalize()
740 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700741 # TODO(maruel): Make this optional.
742 for i in summary['shards']:
743 if i:
744 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700745 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400747 if decorate and total_duration:
748 print('Total duration: %.1fs' % total_duration)
749
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400750 if len(seen_shards) != len(task_ids):
751 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700752 print >> sys.stderr, ('Results from some shards are missing: %s' %
753 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700754 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700755
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400756 return exit_code
maruel@chromium.org0437a732013-08-27 16:05:52 +0000757
758
maruel77f720b2015-09-15 12:35:22 -0700759### API management.
760
761
762class APIError(Exception):
763 pass
764
765
766def endpoints_api_discovery_apis(host):
767 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
768 the APIs exposed by a host.
769
770 https://developers.google.com/discovery/v1/reference/apis/list
771 """
772 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
773 if data is None:
774 raise APIError('Failed to discover APIs on %s' % host)
775 out = {}
776 for api in data['items']:
777 if api['id'] == 'discovery:v1':
778 continue
779 # URL is of the following form:
780 # url = host + (
781 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
782 api_data = net.url_read_json(api['discoveryRestUrl'])
783 if api_data is None:
784 raise APIError('Failed to discover %s on %s' % (api['id'], host))
785 out[api['id']] = api_data
786 return out
787
788
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500789### Commands.
790
791
792def abort_task(_swarming, _manifest):
793 """Given a task manifest that was triggered, aborts its execution."""
794 # TODO(vadimsh): No supported by the server yet.
795
796
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400797def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400798 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500799 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500800 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500801 dest='dimensions', metavar='FOO bar',
802 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500803 parser.add_option_group(parser.filter_group)
804
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400805
Vadim Shtayurab450c602014-05-12 19:23:25 -0700806def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400807 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700808 parser.sharding_group.add_option(
809 '--shards', type='int', default=1,
810 help='Number of shards to trigger and collect.')
811 parser.add_option_group(parser.sharding_group)
812
813
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400814def add_trigger_options(parser):
815 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500816 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400817 add_filter_options(parser)
818
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400819 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500820 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500821 '-s', '--isolated',
822 help='Hash of the .isolated to grab from the isolate server')
823 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500824 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700825 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500826 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500827 '--priority', type='int', default=100,
828 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500829 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500830 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400831 help='Display name of the task. Defaults to '
832 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
833 'isolated file is provided, if a hash is provided, it defaults to '
834 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400835 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400836 '--tags', action='append', default=[],
837 help='Tags to assign to the task.')
838 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500839 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400840 help='User associated with the task. Defaults to authenticated user on '
841 'the server.')
842 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400843 '--idempotent', action='store_true', default=False,
844 help='When set, the server will actively try to find a previous task '
845 'with the same parameter and return this result instead if possible')
846 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400847 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400848 help='Seconds to allow the task to be pending for a bot to run before '
849 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400850 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400851 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400852 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400853 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400854 '--hard-timeout', type='int', default=60*60,
855 help='Seconds to allow the task to complete.')
856 parser.task_group.add_option(
857 '--io-timeout', type='int', default=20*60,
858 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500859 parser.task_group.add_option(
860 '--raw-cmd', action='store_true', default=False,
861 help='When set, the command after -- is used as-is without run_isolated. '
862 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500863 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000864
865
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500866def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500867 """Processes trigger options and uploads files to isolate server if necessary.
868 """
869 options.dimensions = dict(options.dimensions)
870 options.env = dict(options.env)
871
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500872 if not options.dimensions:
873 parser.error('Please at least specify one --dimension')
874 if options.raw_cmd:
875 if not args:
876 parser.error(
877 'Arguments with --raw-cmd should be passed after -- as command '
878 'delimiter.')
879 if options.isolate_server:
880 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
881
882 command = args
883 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500884 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500885 options.user,
886 '_'.join(
887 '%s=%s' % (k, v)
888 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700889 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500890 else:
891 isolateserver.process_isolate_server_options(parser, options, False)
892 try:
maruel77f720b2015-09-15 12:35:22 -0700893 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500894 except ValueError as e:
895 parser.error(str(e))
896
maruel77f720b2015-09-15 12:35:22 -0700897 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
898 # actual command to run.
899 properties = TaskProperties(
900 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500901 dimensions=options.dimensions,
902 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700903 execution_timeout_secs=options.hard_timeout,
904 extra_args=command if inputs_ref else None,
905 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500906 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700907 inputs_ref=inputs_ref,
908 io_timeout_secs=options.io_timeout)
909 return NewTaskRequest(
910 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500911 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700912 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500913 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700914 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500915 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700916 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000917
918
919def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500920 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000921 '-t', '--timeout',
922 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400923 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000924 help='Timeout to wait for result, set to 0 for no timeout; default: '
925 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500926 parser.group_logging.add_option(
927 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700928 parser.group_logging.add_option(
929 '--print-status-updates', action='store_true',
930 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400931 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700932 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700933 '--task-summary-json',
934 metavar='FILE',
935 help='Dump a summary of task results to this file as json. It contains '
936 'only shards statuses as know to server directly. Any output files '
937 'emitted by the task can be collected by using --task-output-dir')
938 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700939 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700940 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700941 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700942 'directory contains per-shard directory with output files produced '
943 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700944 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000945
946
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400947@subcommand.usage('bots...')
948def CMDbot_delete(parser, args):
949 """Forcibly deletes bots from the Swarming server."""
950 parser.add_option(
951 '-f', '--force', action='store_true',
952 help='Do not prompt for confirmation')
953 options, args = parser.parse_args(args)
954 if not args:
955 parser.error('Please specific bots to delete')
956
957 bots = sorted(args)
958 if not options.force:
959 print('Delete the following bots?')
960 for bot in bots:
961 print(' %s' % bot)
962 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
963 print('Goodbye.')
964 return 1
965
966 result = 0
967 for bot in bots:
maruel77f720b2015-09-15 12:35:22 -0700968 url = '%s/_ah/api/swarming/v1/bot/%s' % (options.swarming, bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400969 if net.url_read_json(url, method='DELETE') is None:
970 print('Deleting %s failed' % bot)
971 result = 1
972 return result
973
974
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400975def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400976 """Returns information about the bots connected to the Swarming server."""
977 add_filter_options(parser)
978 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400979 '--dead-only', action='store_true',
980 help='Only print dead bots, useful to reap them and reimage broken bots')
981 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400982 '-k', '--keep-dead', action='store_true',
983 help='Do not filter out dead bots')
984 parser.filter_group.add_option(
985 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400986 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400987 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400988
989 if options.keep_dead and options.dead_only:
990 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700991
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400992 bots = []
993 cursor = None
994 limit = 250
995 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -0700996 base_url = (
997 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400998 while True:
999 url = base_url
1000 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001001 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001002 data = net.url_read_json(url)
1003 if data is None:
1004 print >> sys.stderr, 'Failed to access %s' % options.swarming
1005 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001006 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001007 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001008 if not cursor:
1009 break
1010
maruel77f720b2015-09-15 12:35:22 -07001011 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001012 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001013 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001014 continue
maruel77f720b2015-09-15 12:35:22 -07001015 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001016 continue
1017
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001018 # If the user requested to filter on dimensions, ensure the bot has all the
1019 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001020 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001021 for key, value in options.dimensions:
1022 if key not in dimensions:
1023 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001024 # A bot can have multiple value for a key, for example,
1025 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1026 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001027 if isinstance(dimensions[key], list):
1028 if value not in dimensions[key]:
1029 break
1030 else:
1031 if value != dimensions[key]:
1032 break
1033 else:
maruel77f720b2015-09-15 12:35:22 -07001034 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001035 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001036 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001037 if bot.get('task_id'):
1038 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001039 return 0
1040
1041
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001042@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001043def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001044 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001045
1046 The result can be in multiple part if the execution was sharded. It can
1047 potentially have retries.
1048 """
1049 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001050 parser.add_option(
1051 '-j', '--json',
1052 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001053 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001054 if not args and not options.json:
1055 parser.error('Must specify at least one task id or --json.')
1056 if args and options.json:
1057 parser.error('Only use one of task id or --json.')
1058
1059 if options.json:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001060 try:
1061 with open(options.json) as f:
1062 tasks = sorted(
1063 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1064 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001065 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001066 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001067 else:
1068 valid = frozenset('0123456789abcdef')
1069 if any(not valid.issuperset(task_id) for task_id in args):
1070 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001071
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001072 try:
1073 return collect(
1074 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001075 None,
1076 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001077 options.timeout,
1078 options.decorate,
1079 options.print_status_updates,
1080 options.task_summary_json,
1081 options.task_output_dir)
1082 except Failure:
1083 on_error.report(None)
1084 return 1
1085
1086
maruel77f720b2015-09-15 12:35:22 -07001087@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001088def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001089 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1090 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001091
1092 Examples:
maruel77f720b2015-09-15 12:35:22 -07001093 Listing all bots:
1094 swarming.py query -S https://server-url bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001095
maruel77f720b2015-09-15 12:35:22 -07001096 Listing last 10 tasks on a specific bot named 'swarm1':
1097 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001098 """
1099 CHUNK_SIZE = 250
1100
1101 parser.add_option(
1102 '-L', '--limit', type='int', default=200,
1103 help='Limit to enforce on limitless items (like number of tasks); '
1104 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001105 parser.add_option(
1106 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001107 parser.add_option(
1108 '--progress', action='store_true',
1109 help='Prints a dot at each request to show progress')
1110 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001111 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001112 parser.error(
1113 'Must specify only method name and optionally query args properly '
1114 'escaped.')
1115 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001116 url = base_url
1117 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001118 # Check check, change if not working out.
1119 merge_char = '&' if '?' in url else '?'
1120 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001121 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001122 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001123 # TODO(maruel): Do basic diagnostic.
1124 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001125 return 1
1126
1127 # Some items support cursors. Try to get automatically if cursors are needed
1128 # by looking at the 'cursor' items.
1129 while (
1130 data.get('cursor') and
1131 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001132 merge_char = '&' if '?' in base_url else '?'
1133 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001134 if options.limit:
1135 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001136 if options.progress:
1137 sys.stdout.write('.')
1138 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001139 new = net.url_read_json(url)
1140 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001141 if options.progress:
1142 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001143 print >> sys.stderr, 'Failed to access %s' % options.swarming
1144 return 1
1145 data['items'].extend(new['items'])
maruel77f720b2015-09-15 12:35:22 -07001146 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001147
maruel77f720b2015-09-15 12:35:22 -07001148 if options.progress:
1149 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001150 if options.limit and len(data.get('items', [])) > options.limit:
1151 data['items'] = data['items'][:options.limit]
1152 data.pop('cursor', None)
1153
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001154 if options.json:
maruel77f720b2015-09-15 12:35:22 -07001155 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001156 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001157 try:
maruel77f720b2015-09-15 12:35:22 -07001158 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001159 sys.stdout.write('\n')
1160 except IOError:
1161 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001162 return 0
1163
1164
maruel77f720b2015-09-15 12:35:22 -07001165def CMDquery_list(parser, args):
1166 """Returns list of all the Swarming APIs that can be used with command
1167 'query'.
1168 """
1169 parser.add_option(
1170 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1171 options, args = parser.parse_args(args)
1172 if args:
1173 parser.error('No argument allowed.')
1174
1175 try:
1176 apis = endpoints_api_discovery_apis(options.swarming)
1177 except APIError as e:
1178 parser.error(str(e))
1179 if options.json:
1180 with open(options.json, 'wb') as f:
1181 json.dump(apis, f)
1182 else:
1183 help_url = (
1184 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1185 options.swarming)
1186 for api_id, api in sorted(apis.iteritems()):
1187 print api_id
1188 print ' ' + api['description']
1189 for resource_name, resource in sorted(api['resources'].iteritems()):
1190 print ''
1191 for method_name, method in sorted(resource['methods'].iteritems()):
1192 # Only list the GET ones.
1193 if method['httpMethod'] != 'GET':
1194 continue
1195 print '- %s.%s: %s' % (
1196 resource_name, method_name, method['path'])
1197 print ' ' + method['description']
1198 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1199 return 0
1200
1201
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001202@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001203def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001204 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001205
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001206 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001207 """
1208 add_trigger_options(parser)
1209 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001210 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001211 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001212 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001213 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001214 tasks = trigger_task_shards(
1215 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001216 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001217 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001218 'Failed to trigger %s(%s): %s' %
1219 (options.task_name, args[0], e.args[0]))
1220 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001221 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001222 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001223 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001224 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001225 task_ids = [
1226 t['task_id']
1227 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1228 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001229 try:
1230 return collect(
1231 options.swarming,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001232 options.task_name,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001233 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001234 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001235 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001236 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001237 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001238 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001239 except Failure:
1240 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001241 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001242
1243
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001244@subcommand.usage('task_id')
1245def CMDreproduce(parser, args):
1246 """Runs a task locally that was triggered on the server.
1247
1248 This running locally the same commands that have been run on the bot. The data
1249 downloaded will be in a subdirectory named 'work' of the current working
1250 directory.
1251 """
1252 options, args = parser.parse_args(args)
1253 if len(args) != 1:
1254 parser.error('Must specify exactly one task id.')
1255
maruel77f720b2015-09-15 12:35:22 -07001256 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001257 request = net.url_read_json(url)
1258 if not request:
1259 print >> sys.stderr, 'Failed to retrieve request data for the task'
1260 return 1
1261
1262 if not os.path.isdir('work'):
1263 os.mkdir('work')
1264
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001265 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001266 env = None
1267 if properties['env']:
1268 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001269 logging.info('env: %r', properties['env'])
1270 env.update(
maruel77f720b2015-09-15 12:35:22 -07001271 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1272 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001273
maruel77f720b2015-09-15 12:35:22 -07001274 try:
1275 return subprocess.call(properties['command'], env=env, cwd='work')
1276 except OSError as e:
1277 print >> sys.stderr, 'Failed to run: %s' % ' '.join(properties['command'])
1278 print >> sys.stderr, str(e)
1279 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001280
1281
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001282@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001283def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001284 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001285
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001286 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001287 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001288
1289 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001290
1291 Passes all extra arguments provided after '--' as additional command line
1292 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001293 """
1294 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001295 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001296 parser.add_option(
1297 '--dump-json',
1298 metavar='FILE',
1299 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001300 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001301 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001302 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001303 tasks = trigger_task_shards(
1304 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001305 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001306 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001307 tasks_sorted = sorted(
1308 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001309 if options.dump_json:
1310 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001311 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001312 'tasks': tasks,
1313 }
1314 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001315 print('To collect results, use:')
1316 print(' swarming.py collect -S %s --json %s' %
1317 (options.swarming, options.dump_json))
1318 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001319 print('To collect results, use:')
1320 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001321 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1322 print('Or visit:')
1323 for t in tasks_sorted:
1324 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001325 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001326 except Failure:
1327 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001328 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001329
1330
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001331class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001332 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001333 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001334 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001335 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001336 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001337 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001338 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001339 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001340 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001341 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001342
1343 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001344 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001345 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001346 auth.process_auth_options(self, options)
1347 user = self._process_swarming(options)
1348 if hasattr(options, 'user') and not options.user:
1349 options.user = user
1350 return options, args
1351
1352 def _process_swarming(self, options):
1353 """Processes the --swarming option and aborts if not specified.
1354
1355 Returns the identity as determined by the server.
1356 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001357 if not options.swarming:
1358 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001359 try:
1360 options.swarming = net.fix_url(options.swarming)
1361 except ValueError as e:
1362 self.error('--swarming %s' % e)
1363 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001364 try:
1365 user = auth.ensure_logged_in(options.swarming)
1366 except ValueError as e:
1367 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001368 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001369
1370
1371def main(args):
1372 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001373 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001374
1375
1376if __name__ == '__main__':
1377 fix_encoding.fix_encoding()
1378 tools.disable_buffering()
1379 colorama.init()
1380 sys.exit(main(sys.argv[1:]))