blob: e9e2612526088c44bb1fe99344cdbf2517cdd322 [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'])
578 # tags
579 result['try_number'] = (
580 int(result['try_number']) if result['try_number'] else None)
581 result['bot_dimensions'] = {
582 i['key']: i['value'] for i in result['bot_dimensions']
583 }
584
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(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400699 swarming, task_name, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400700 task_summary_json, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500701 """Retrieves results of a Swarming task."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700702 # Collect summary JSON and output files (if task_output_dir is not None).
703 output_collector = TaskOutputCollector(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400704 task_output_dir, task_name, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700705
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700706 seen_shards = set()
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400707 exit_code = 0
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400708 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700709 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400710 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400711 swarming, task_ids, timeout, None, print_status_updates,
712 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700713 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700714
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400715 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700716 shard_exit_code = metadata.get('exit_code')
717 if shard_exit_code:
718 shard_exit_code = int(shard_exit_code)
maruel8db72b72015-09-02 13:28:11 -0700719 if shard_exit_code:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400720 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700721 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700722
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700723 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400724 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400725 if len(seen_shards) < len(task_ids):
726 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700727 else:
maruel77f720b2015-09-15 12:35:22 -0700728 print('%s: %s %s' % (
729 metadata.get('bot_id', 'N/A'),
730 metadata['task_id'],
731 shard_exit_code))
732 if metadata['output']:
733 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400734 if output:
735 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700736 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700737 summary = output_collector.finalize()
738 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700739 # TODO(maruel): Make this optional.
740 for i in summary['shards']:
741 if i:
742 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700743 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700744
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400745 if decorate and total_duration:
746 print('Total duration: %.1fs' % total_duration)
747
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400748 if len(seen_shards) != len(task_ids):
749 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700750 print >> sys.stderr, ('Results from some shards are missing: %s' %
751 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700752 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700753
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400754 return exit_code
maruel@chromium.org0437a732013-08-27 16:05:52 +0000755
756
maruel77f720b2015-09-15 12:35:22 -0700757### API management.
758
759
760class APIError(Exception):
761 pass
762
763
764def endpoints_api_discovery_apis(host):
765 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
766 the APIs exposed by a host.
767
768 https://developers.google.com/discovery/v1/reference/apis/list
769 """
770 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
771 if data is None:
772 raise APIError('Failed to discover APIs on %s' % host)
773 out = {}
774 for api in data['items']:
775 if api['id'] == 'discovery:v1':
776 continue
777 # URL is of the following form:
778 # url = host + (
779 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
780 api_data = net.url_read_json(api['discoveryRestUrl'])
781 if api_data is None:
782 raise APIError('Failed to discover %s on %s' % (api['id'], host))
783 out[api['id']] = api_data
784 return out
785
786
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500787### Commands.
788
789
790def abort_task(_swarming, _manifest):
791 """Given a task manifest that was triggered, aborts its execution."""
792 # TODO(vadimsh): No supported by the server yet.
793
794
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400795def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400796 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500797 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500798 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500799 dest='dimensions', metavar='FOO bar',
800 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500801 parser.add_option_group(parser.filter_group)
802
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400803
Vadim Shtayurab450c602014-05-12 19:23:25 -0700804def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400805 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700806 parser.sharding_group.add_option(
807 '--shards', type='int', default=1,
808 help='Number of shards to trigger and collect.')
809 parser.add_option_group(parser.sharding_group)
810
811
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400812def add_trigger_options(parser):
813 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500814 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400815 add_filter_options(parser)
816
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400817 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500818 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500819 '-s', '--isolated',
820 help='Hash of the .isolated to grab from the isolate server')
821 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500822 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700823 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500824 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500825 '--priority', type='int', default=100,
826 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500827 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500828 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400829 help='Display name of the task. Defaults to '
830 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
831 'isolated file is provided, if a hash is provided, it defaults to '
832 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400833 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400834 '--tags', action='append', default=[],
835 help='Tags to assign to the task.')
836 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500837 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400838 help='User associated with the task. Defaults to authenticated user on '
839 'the server.')
840 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400841 '--idempotent', action='store_true', default=False,
842 help='When set, the server will actively try to find a previous task '
843 'with the same parameter and return this result instead if possible')
844 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400845 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400846 help='Seconds to allow the task to be pending for a bot to run before '
847 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400848 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400849 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400850 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400851 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400852 '--hard-timeout', type='int', default=60*60,
853 help='Seconds to allow the task to complete.')
854 parser.task_group.add_option(
855 '--io-timeout', type='int', default=20*60,
856 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500857 parser.task_group.add_option(
858 '--raw-cmd', action='store_true', default=False,
859 help='When set, the command after -- is used as-is without run_isolated. '
860 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500861 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000862
863
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500864def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500865 """Processes trigger options and uploads files to isolate server if necessary.
866 """
867 options.dimensions = dict(options.dimensions)
868 options.env = dict(options.env)
869
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500870 if not options.dimensions:
871 parser.error('Please at least specify one --dimension')
872 if options.raw_cmd:
873 if not args:
874 parser.error(
875 'Arguments with --raw-cmd should be passed after -- as command '
876 'delimiter.')
877 if options.isolate_server:
878 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
879
880 command = args
881 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500882 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500883 options.user,
884 '_'.join(
885 '%s=%s' % (k, v)
886 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700887 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500888 else:
889 isolateserver.process_isolate_server_options(parser, options, False)
890 try:
maruel77f720b2015-09-15 12:35:22 -0700891 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500892 except ValueError as e:
893 parser.error(str(e))
894
maruel77f720b2015-09-15 12:35:22 -0700895 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
896 # actual command to run.
897 properties = TaskProperties(
898 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500899 dimensions=options.dimensions,
900 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700901 execution_timeout_secs=options.hard_timeout,
902 extra_args=command if inputs_ref else None,
903 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500904 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700905 inputs_ref=inputs_ref,
906 io_timeout_secs=options.io_timeout)
907 return NewTaskRequest(
908 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500909 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700910 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500911 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700912 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500913 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700914 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000915
916
917def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500918 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000919 '-t', '--timeout',
920 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400921 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000922 help='Timeout to wait for result, set to 0 for no timeout; default: '
923 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500924 parser.group_logging.add_option(
925 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700926 parser.group_logging.add_option(
927 '--print-status-updates', action='store_true',
928 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400929 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700930 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700931 '--task-summary-json',
932 metavar='FILE',
933 help='Dump a summary of task results to this file as json. It contains '
934 'only shards statuses as know to server directly. Any output files '
935 'emitted by the task can be collected by using --task-output-dir')
936 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700937 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700938 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700939 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700940 'directory contains per-shard directory with output files produced '
941 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700942 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000943
944
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400945@subcommand.usage('bots...')
946def CMDbot_delete(parser, args):
947 """Forcibly deletes bots from the Swarming server."""
948 parser.add_option(
949 '-f', '--force', action='store_true',
950 help='Do not prompt for confirmation')
951 options, args = parser.parse_args(args)
952 if not args:
953 parser.error('Please specific bots to delete')
954
955 bots = sorted(args)
956 if not options.force:
957 print('Delete the following bots?')
958 for bot in bots:
959 print(' %s' % bot)
960 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
961 print('Goodbye.')
962 return 1
963
964 result = 0
965 for bot in bots:
maruel77f720b2015-09-15 12:35:22 -0700966 url = '%s/_ah/api/swarming/v1/bot/%s' % (options.swarming, bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400967 if net.url_read_json(url, method='DELETE') is None:
968 print('Deleting %s failed' % bot)
969 result = 1
970 return result
971
972
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400973def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400974 """Returns information about the bots connected to the Swarming server."""
975 add_filter_options(parser)
976 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400977 '--dead-only', action='store_true',
978 help='Only print dead bots, useful to reap them and reimage broken bots')
979 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400980 '-k', '--keep-dead', action='store_true',
981 help='Do not filter out dead bots')
982 parser.filter_group.add_option(
983 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400984 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400985 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400986
987 if options.keep_dead and options.dead_only:
988 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700989
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400990 bots = []
991 cursor = None
992 limit = 250
993 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -0700994 base_url = (
995 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400996 while True:
997 url = base_url
998 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400999 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001000 data = net.url_read_json(url)
1001 if data is None:
1002 print >> sys.stderr, 'Failed to access %s' % options.swarming
1003 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001004 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001005 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001006 if not cursor:
1007 break
1008
maruel77f720b2015-09-15 12:35:22 -07001009 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001010 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001011 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001012 continue
maruel77f720b2015-09-15 12:35:22 -07001013 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001014 continue
1015
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001016 # If the user requested to filter on dimensions, ensure the bot has all the
1017 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001018 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001019 for key, value in options.dimensions:
1020 if key not in dimensions:
1021 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001022 # A bot can have multiple value for a key, for example,
1023 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1024 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001025 if isinstance(dimensions[key], list):
1026 if value not in dimensions[key]:
1027 break
1028 else:
1029 if value != dimensions[key]:
1030 break
1031 else:
maruel77f720b2015-09-15 12:35:22 -07001032 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001033 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001034 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001035 if bot.get('task_id'):
1036 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001037 return 0
1038
1039
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001040@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001041def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001042 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001043
1044 The result can be in multiple part if the execution was sharded. It can
1045 potentially have retries.
1046 """
1047 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001048 parser.add_option(
1049 '-j', '--json',
1050 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001051 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001052 if not args and not options.json:
1053 parser.error('Must specify at least one task id or --json.')
1054 if args and options.json:
1055 parser.error('Only use one of task id or --json.')
1056
1057 if options.json:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001058 try:
1059 with open(options.json) as f:
1060 tasks = sorted(
1061 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1062 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001063 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001064 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001065 else:
1066 valid = frozenset('0123456789abcdef')
1067 if any(not valid.issuperset(task_id) for task_id in args):
1068 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001069
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001070 try:
1071 return collect(
1072 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001073 None,
1074 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001075 options.timeout,
1076 options.decorate,
1077 options.print_status_updates,
1078 options.task_summary_json,
1079 options.task_output_dir)
1080 except Failure:
1081 on_error.report(None)
1082 return 1
1083
1084
maruel77f720b2015-09-15 12:35:22 -07001085@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001086def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001087 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1088 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001089
1090 Examples:
maruel77f720b2015-09-15 12:35:22 -07001091 Listing all bots:
1092 swarming.py query -S https://server-url bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001093
maruel77f720b2015-09-15 12:35:22 -07001094 Listing last 10 tasks on a specific bot named 'swarm1':
1095 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001096 """
1097 CHUNK_SIZE = 250
1098
1099 parser.add_option(
1100 '-L', '--limit', type='int', default=200,
1101 help='Limit to enforce on limitless items (like number of tasks); '
1102 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001103 parser.add_option(
1104 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001105 parser.add_option(
1106 '--progress', action='store_true',
1107 help='Prints a dot at each request to show progress')
1108 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001109 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001110 parser.error(
1111 'Must specify only method name and optionally query args properly '
1112 'escaped.')
1113 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001114 url = base_url
1115 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001116 # Check check, change if not working out.
1117 merge_char = '&' if '?' in url else '?'
1118 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001119 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001120 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001121 # TODO(maruel): Do basic diagnostic.
1122 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001123 return 1
1124
1125 # Some items support cursors. Try to get automatically if cursors are needed
1126 # by looking at the 'cursor' items.
1127 while (
1128 data.get('cursor') and
1129 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001130 merge_char = '&' if '?' in base_url else '?'
1131 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001132 if options.limit:
1133 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001134 if options.progress:
1135 sys.stdout.write('.')
1136 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001137 new = net.url_read_json(url)
1138 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001139 if options.progress:
1140 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001141 print >> sys.stderr, 'Failed to access %s' % options.swarming
1142 return 1
1143 data['items'].extend(new['items'])
maruel77f720b2015-09-15 12:35:22 -07001144 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001145
maruel77f720b2015-09-15 12:35:22 -07001146 if options.progress:
1147 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001148 if options.limit and len(data.get('items', [])) > options.limit:
1149 data['items'] = data['items'][:options.limit]
1150 data.pop('cursor', None)
1151
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001152 if options.json:
maruel77f720b2015-09-15 12:35:22 -07001153 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001154 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001155 try:
maruel77f720b2015-09-15 12:35:22 -07001156 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001157 sys.stdout.write('\n')
1158 except IOError:
1159 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001160 return 0
1161
1162
maruel77f720b2015-09-15 12:35:22 -07001163def CMDquery_list(parser, args):
1164 """Returns list of all the Swarming APIs that can be used with command
1165 'query'.
1166 """
1167 parser.add_option(
1168 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1169 options, args = parser.parse_args(args)
1170 if args:
1171 parser.error('No argument allowed.')
1172
1173 try:
1174 apis = endpoints_api_discovery_apis(options.swarming)
1175 except APIError as e:
1176 parser.error(str(e))
1177 if options.json:
1178 with open(options.json, 'wb') as f:
1179 json.dump(apis, f)
1180 else:
1181 help_url = (
1182 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1183 options.swarming)
1184 for api_id, api in sorted(apis.iteritems()):
1185 print api_id
1186 print ' ' + api['description']
1187 for resource_name, resource in sorted(api['resources'].iteritems()):
1188 print ''
1189 for method_name, method in sorted(resource['methods'].iteritems()):
1190 # Only list the GET ones.
1191 if method['httpMethod'] != 'GET':
1192 continue
1193 print '- %s.%s: %s' % (
1194 resource_name, method_name, method['path'])
1195 print ' ' + method['description']
1196 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1197 return 0
1198
1199
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001200@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001201def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001202 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001203
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001204 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001205 """
1206 add_trigger_options(parser)
1207 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001208 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001209 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001210 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001211 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001212 tasks = trigger_task_shards(
1213 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001214 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001215 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001216 'Failed to trigger %s(%s): %s' %
1217 (options.task_name, args[0], e.args[0]))
1218 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001219 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001220 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001221 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001222 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001223 task_ids = [
1224 t['task_id']
1225 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1226 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001227 try:
1228 return collect(
1229 options.swarming,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001230 options.task_name,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001231 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001232 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001233 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001234 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001235 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001236 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001237 except Failure:
1238 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001239 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001240
1241
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001242@subcommand.usage('task_id')
1243def CMDreproduce(parser, args):
1244 """Runs a task locally that was triggered on the server.
1245
1246 This running locally the same commands that have been run on the bot. The data
1247 downloaded will be in a subdirectory named 'work' of the current working
1248 directory.
1249 """
1250 options, args = parser.parse_args(args)
1251 if len(args) != 1:
1252 parser.error('Must specify exactly one task id.')
1253
maruel77f720b2015-09-15 12:35:22 -07001254 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001255 request = net.url_read_json(url)
1256 if not request:
1257 print >> sys.stderr, 'Failed to retrieve request data for the task'
1258 return 1
1259
1260 if not os.path.isdir('work'):
1261 os.mkdir('work')
1262
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001263 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001264 env = None
1265 if properties['env']:
1266 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001267 logging.info('env: %r', properties['env'])
1268 env.update(
maruel77f720b2015-09-15 12:35:22 -07001269 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1270 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001271
maruel77f720b2015-09-15 12:35:22 -07001272 try:
1273 return subprocess.call(properties['command'], env=env, cwd='work')
1274 except OSError as e:
1275 print >> sys.stderr, 'Failed to run: %s' % ' '.join(properties['command'])
1276 print >> sys.stderr, str(e)
1277 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001278
1279
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001280@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001281def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001282 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001283
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001284 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001285 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001286
1287 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001288
1289 Passes all extra arguments provided after '--' as additional command line
1290 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001291 """
1292 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001293 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001294 parser.add_option(
1295 '--dump-json',
1296 metavar='FILE',
1297 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001298 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001299 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001300 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001301 tasks = trigger_task_shards(
1302 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001303 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001304 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001305 tasks_sorted = sorted(
1306 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001307 if options.dump_json:
1308 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001309 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001310 'tasks': tasks,
1311 }
1312 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001313 print('To collect results, use:')
1314 print(' swarming.py collect -S %s --json %s' %
1315 (options.swarming, options.dump_json))
1316 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001317 print('To collect results, use:')
1318 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001319 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1320 print('Or visit:')
1321 for t in tasks_sorted:
1322 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001323 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001324 except Failure:
1325 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001326 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001327
1328
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001329class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001330 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001331 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001332 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001333 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001334 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001335 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001336 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001337 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001338 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001339 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001340
1341 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001342 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001343 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001344 auth.process_auth_options(self, options)
1345 user = self._process_swarming(options)
1346 if hasattr(options, 'user') and not options.user:
1347 options.user = user
1348 return options, args
1349
1350 def _process_swarming(self, options):
1351 """Processes the --swarming option and aborts if not specified.
1352
1353 Returns the identity as determined by the server.
1354 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001355 if not options.swarming:
1356 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001357 try:
1358 options.swarming = net.fix_url(options.swarming)
1359 except ValueError as e:
1360 self.error('--swarming %s' % e)
1361 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001362 try:
1363 user = auth.ensure_logged_in(options.swarming)
1364 except ValueError as e:
1365 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001366 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001367
1368
1369def main(args):
1370 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001371 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001372
1373
1374if __name__ == '__main__':
1375 fix_encoding.fix_encoding()
1376 tools.disable_buffering()
1377 colorama.init()
1378 sys.exit(main(sys.argv[1:]))