blob: 6d4c51bb73e9dd120aad4a345d24e353a4c57756 [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
maruel9ba953f2015-09-10 14:01:58 -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
maruelf33f3f62015-09-10 11:33:46 -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'):
maruelf33f3f62015-09-10 11:33:46 -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:
maruelf33f3f62015-09-10 11:33:46 -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(
maruelf33f3f62015-09-10 11:33:46 -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
maruelf33f3f62015-09-10 11:33:46 -0700119 inputs_ref = {
120 'isolated': options.isolated,
121 'isolatedserver': options.isolate_server,
122 'namespace': options.namespace,
123 }
124 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500125
126
127### Triggering.
128
129
130TaskRequest = collections.namedtuple(
131 'TaskRequest',
132 [
133 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500134 'dimensions',
135 'env',
136 'expiration',
maruelf33f3f62015-09-10 11:33:46 -0700137 'extra_args',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500138 'hard_timeout',
139 'idempotent',
maruelf33f3f62015-09-10 11:33:46 -0700140 'inputs_ref',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500141 'io_timeout',
142 'name',
143 'priority',
144 'tags',
145 'user',
146 'verbose',
147 ])
148
149
150def task_request_to_raw_request(task_request):
151 """Returns the json dict expected by the Swarming server for new request.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500152 """
153 return {
154 'name': task_request.name,
Marc-Antoine Rueld863df32015-01-24 20:34:48 -0500155 'parent_task_id': os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500156 'priority': task_request.priority,
157 'properties': {
maruelf33f3f62015-09-10 11:33:46 -0700158 'command': task_request.command,
159 'dimensions': [
160 {'key': k, 'value': v}
161 for k, v in sorted(task_request.dimensions.iteritems())
162 ],
163 'env': [
164 {'key': k, 'value': v}
165 for k, v in sorted(task_request.env.iteritems())
166 ],
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500167 'execution_timeout_secs': task_request.hard_timeout,
marueld8aba222015-09-03 12:21:19 -0700168 'idempotent': task_request.idempotent,
maruelf33f3f62015-09-10 11:33:46 -0700169 'inputs_ref': task_request.inputs_ref,
170 'io_timeout_secs': task_request.io_timeout,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 },
maruelf33f3f62015-09-10 11:33:46 -0700172 'expiration_secs': task_request.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500173 'tags': task_request.tags,
174 'user': task_request.user,
175 }
176
177
maruelf33f3f62015-09-10 11:33:46 -0700178def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500179 """Triggers a request on the Swarming server and returns the json data.
180
181 It's the low-level function.
182
183 Returns:
184 {
185 'request': {
186 'created_ts': u'2010-01-02 03:04:05',
187 'name': ..
188 },
189 'task_id': '12300',
190 }
191 """
192 logging.info('Triggering: %s', raw_request['name'])
193
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500194 result = net.url_read_json(
maruelf33f3f62015-09-10 11:33:46 -0700195 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500196 if not result:
197 on_error.report('Failed to trigger task %s' % raw_request['name'])
198 return None
199 return result
200
201
202def setup_googletest(env, shards, index):
203 """Sets googletest specific environment variables."""
204 if shards > 1:
205 env = env.copy()
206 env['GTEST_SHARD_INDEX'] = str(index)
207 env['GTEST_TOTAL_SHARDS'] = str(shards)
208 return env
209
210
211def trigger_task_shards(swarming, task_request, shards):
212 """Triggers one or many subtasks of a sharded task.
213
214 Returns:
215 Dict with task details, returned to caller as part of --dump-json output.
216 None in case of failure.
217 """
218 def convert(index):
219 req = task_request
220 if shards > 1:
221 req = req._replace(
222 env=setup_googletest(req.env, shards, index),
223 name='%s:%s:%s' % (req.name, index, shards))
224 return task_request_to_raw_request(req)
225
226 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500227 tasks = {}
228 priority_warning = False
229 for index, request in enumerate(requests):
maruelf33f3f62015-09-10 11:33:46 -0700230 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500231 if not task:
232 break
233 logging.info('Request result: %s', task)
234 if (not priority_warning and
235 task['request']['priority'] != task_request.priority):
236 priority_warning = True
237 print >> sys.stderr, (
238 'Priority was reset to %s' % task['request']['priority'])
239 tasks[request['name']] = {
240 'shard_index': index,
241 'task_id': task['task_id'],
242 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
243 }
244
245 # Some shards weren't triggered. Abort everything.
246 if len(tasks) != len(requests):
247 if tasks:
248 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
249 len(tasks), len(requests))
250 for task_dict in tasks.itervalues():
251 abort_task(swarming, task_dict['task_id'])
252 return None
253
254 return tasks
255
256
257### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000258
259
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700260# How often to print status updates to stdout in 'collect'.
261STATUS_UPDATE_INTERVAL = 15 * 60.
262
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400263
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400264class State(object):
265 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000266
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400267 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
268 values are part of the API so if they change, the API changed.
269
270 It's in fact an enum. Values should be in decreasing order of importance.
271 """
272 RUNNING = 0x10
273 PENDING = 0x20
274 EXPIRED = 0x30
275 TIMED_OUT = 0x40
276 BOT_DIED = 0x50
277 CANCELED = 0x60
278 COMPLETED = 0x70
279
maruelf33f3f62015-09-10 11:33:46 -0700280 STATES = (
281 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
282 'COMPLETED')
283 STATES_RUNNING = ('RUNNING', 'PENDING')
284 STATES_NOT_RUNNING = (
285 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
286 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
287 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400288
289 _NAMES = {
290 RUNNING: 'Running',
291 PENDING: 'Pending',
292 EXPIRED: 'Expired',
293 TIMED_OUT: 'Execution timed out',
294 BOT_DIED: 'Bot died',
295 CANCELED: 'User canceled',
296 COMPLETED: 'Completed',
297 }
298
maruelf33f3f62015-09-10 11:33:46 -0700299 _ENUMS = {
300 'RUNNING': RUNNING,
301 'PENDING': PENDING,
302 'EXPIRED': EXPIRED,
303 'TIMED_OUT': TIMED_OUT,
304 'BOT_DIED': BOT_DIED,
305 'CANCELED': CANCELED,
306 'COMPLETED': COMPLETED,
307 }
308
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400309 @classmethod
310 def to_string(cls, state):
311 """Returns a user-readable string representing a State."""
312 if state not in cls._NAMES:
313 raise ValueError('Invalid state %s' % state)
314 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000315
maruelf33f3f62015-09-10 11:33:46 -0700316 @classmethod
317 def from_enum(cls, state):
318 """Returns int value based on the string."""
319 if state not in cls._ENUMS:
320 raise ValueError('Invalid state %s' % state)
321 return cls._ENUMS[state]
322
maruel@chromium.org0437a732013-08-27 16:05:52 +0000323
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700324class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700325 """Assembles task execution summary (for --task-summary-json output).
326
327 Optionally fetches task outputs from isolate server to local disk (used when
328 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700329
330 This object is shared among multiple threads running 'retrieve_results'
331 function, in particular they call 'process_shard_result' method in parallel.
332 """
333
334 def __init__(self, task_output_dir, task_name, shard_count):
335 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
336
337 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700338 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700339 task_name: name of the swarming task results belong to.
340 shard_count: expected number of task shards.
341 """
342 self.task_output_dir = task_output_dir
343 self.task_name = task_name
344 self.shard_count = shard_count
345
346 self._lock = threading.Lock()
347 self._per_shard_results = {}
348 self._storage = None
349
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700350 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700351 os.makedirs(self.task_output_dir)
352
Vadim Shtayurab450c602014-05-12 19:23:25 -0700353 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700354 """Stores results of a single task shard, fetches output files if necessary.
355
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400356 Modifies |result| in place.
357
maruelf33f3f62015-09-10 11:33:46 -0700358 shard_index is 0-based.
359
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360 Called concurrently from multiple threads.
361 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700363 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364 if shard_index < 0 or shard_index >= self.shard_count:
365 logging.warning(
366 'Shard index %d is outside of expected range: [0; %d]',
367 shard_index, self.shard_count - 1)
368 return
369
maruelf33f3f62015-09-10 11:33:46 -0700370 if result.get('outputs_ref'):
371 ref = result['outputs_ref']
372 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
373 ref['isolatedserver'],
374 urllib.urlencode(
375 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400376
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377 # Store result dict of that shard, ignore results we've already seen.
378 with self._lock:
379 if shard_index in self._per_shard_results:
380 logging.warning('Ignoring duplicate shard index %d', shard_index)
381 return
382 self._per_shard_results[shard_index] = result
383
384 # Fetch output files if necessary.
maruelf33f3f62015-09-10 11:33:46 -0700385 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400386 storage = self._get_storage(
maruelf33f3f62015-09-10 11:33:46 -0700387 result['outputs_ref']['isolatedserver'],
388 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400389 if storage:
390 # Output files are supposed to be small and they are not reused across
391 # tasks. So use MemoryCache for them instead of on-disk cache. Make
392 # files writable, so that calling script can delete them.
393 isolateserver.fetch_isolated(
maruelf33f3f62015-09-10 11:33:46 -0700394 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400395 storage,
396 isolateserver.MemoryCache(file_mode_mask=0700),
397 os.path.join(self.task_output_dir, str(shard_index)),
398 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399
400 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700401 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 with self._lock:
403 # Write an array of shard results with None for missing shards.
404 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700405 'shards': [
406 self._per_shard_results.get(i) for i in xrange(self.shard_count)
407 ],
408 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700409 # Write summary.json to task_output_dir as well.
410 if self.task_output_dir:
411 tools.write_json(
412 os.path.join(self.task_output_dir, 'summary.json'),
413 summary,
414 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700415 if self._storage:
416 self._storage.close()
417 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700418 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700419
420 def _get_storage(self, isolate_server, namespace):
421 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700422 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700423 with self._lock:
424 if not self._storage:
425 self._storage = isolateserver.get_storage(isolate_server, namespace)
426 else:
427 # Shards must all use exact same isolate server and namespace.
428 if self._storage.location != isolate_server:
429 logging.error(
430 'Task shards are using multiple isolate servers: %s and %s',
431 self._storage.location, isolate_server)
432 return None
433 if self._storage.namespace != namespace:
434 logging.error(
435 'Task shards are using multiple namespaces: %s and %s',
436 self._storage.namespace, namespace)
437 return None
438 return self._storage
439
440
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500441def now():
442 """Exists so it can be mocked easily."""
443 return time.time()
444
445
maruel50cb24b2015-09-10 12:04:16 -0700446def parse_time(value):
447 """Converts serialized time from the API to datetime.datetime."""
448 # When microseconds are 0, the '.123456' suffix is elided. This means the
449 # serialized format is not consistent, which confuses the hell out of python.
450 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
451 try:
452 return datetime.datetime.strptime(value, fmt)
453 except ValueError:
454 pass
455 raise ValueError('Failed to parse %s' % value)
456
457
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700458def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400459 base_url, shard_index, task_id, timeout, should_stop, output_collector):
460 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700461
Vadim Shtayurab450c602014-05-12 19:23:25 -0700462 Returns:
463 <result dict> on success.
464 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700465 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000466 assert isinstance(timeout, float), timeout
maruelf33f3f62015-09-10 11:33:46 -0700467 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
468 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700469 started = now()
470 deadline = started + timeout if timeout else None
471 attempt = 0
472
473 while not should_stop.is_set():
474 attempt += 1
475
476 # Waiting for too long -> give up.
477 current_time = now()
478 if deadline and current_time >= deadline:
479 logging.error('retrieve_results(%s) timed out on attempt %d',
480 base_url, attempt)
481 return None
482
483 # Do not spin too fast. Spin faster at the beginning though.
484 # Start with 1 sec delay and for each 30 sec of waiting add another second
485 # of delay, until hitting 15 sec ceiling.
486 if attempt > 1:
487 max_delay = min(15, 1 + (current_time - started) / 30.0)
488 delay = min(max_delay, deadline - current_time) if deadline else max_delay
489 if delay > 0:
490 logging.debug('Waiting %.1f sec before retrying', delay)
491 should_stop.wait(delay)
492 if should_stop.is_set():
493 return None
494
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400495 # Disable internal retries in net.url_read_json, since we are doing retries
496 # ourselves.
497 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
498 result = net.url_read_json(result_url, retry_50x=False)
499 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400500 continue
maruelf33f3f62015-09-10 11:33:46 -0700501
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400502 if result['state'] in State.STATES_NOT_RUNNING:
maruelf33f3f62015-09-10 11:33:46 -0700503 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400504 out = net.url_read_json(output_url)
maruelf33f3f62015-09-10 11:33:46 -0700505 result['output'] = out['output'] if out else out
506 if not result['output']:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400507 logging.error('No output found for task %s', task_id)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700508 # Record the result, try to fetch attached output files (if any).
509 if output_collector:
510 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700511 output_collector.process_shard_result(shard_index, result)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700512 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000513
514
maruelf33f3f62015-09-10 11:33:46 -0700515def convert_to_old_format(result):
516 """Converts the task result data from Endpoints API format to old API format
517 for compatibility.
518
519 This goes into the file generated as --task-summary-json.
520 """
521 # Sets default.
522 result.setdefault('abandoned_ts', None)
523 result.setdefault('bot_id', None)
524 result.setdefault('bot_version', None)
525 result.setdefault('children_task_ids', [])
526 result.setdefault('completed_ts', None)
527 result.setdefault('cost_saved_usd', None)
528 result.setdefault('costs_usd', None)
529 result.setdefault('deduped_from', None)
530 result.setdefault('name', None)
531 result.setdefault('outputs_ref', None)
532 result.setdefault('properties_hash', None)
533 result.setdefault('server_versions', None)
534 result.setdefault('started_ts', None)
535 result.setdefault('tags', None)
536 result.setdefault('user', None)
537
538 # Convertion back to old API.
539 result['durations'] = [result.pop('duration')]
540 exit_code = result.pop('exit_code')
541 result['exit_codes'] = [int(exit_code)] if exit_code else []
542 result['id'] = result.pop('task_id')
543 result['isolated_out'] = result.get('outputs_ref', None)
544 result['outputs'] = [result.pop('output')]
545 # properties_hash
546 # server_version
547 # Endpoints result 'state' as string. For compatibility with old code, convert
548 # to int.
549 result['state'] = State.from_enum(result['state'])
550 # tags
551 result['try_number'] = (
552 int(result['try_number']) if result['try_number'] else None)
553 result['bot_dimensions'] = {
554 i['key']: i['value'] for i in result['bot_dimensions']
555 }
556
557
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700558def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400559 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
560 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500561 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000562
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700563 Duplicate shards are ignored. Shards are yielded in order of completion.
564 Timed out shards are NOT yielded at all. Caller can compare number of yielded
565 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000566
567 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500568 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 +0000569 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500570
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700571 output_collector is an optional instance of TaskOutputCollector that will be
572 used to fetch files produced by a task from isolate server to the local disk.
573
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500574 Yields:
575 (index, result). In particular, 'result' is defined as the
576 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000577 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000578 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400579 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700580 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700581 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700582
maruel@chromium.org0437a732013-08-27 16:05:52 +0000583 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
584 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700585 # Adds a task to the thread pool to call 'retrieve_results' and return
586 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400587 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700588 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000589 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400590 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
591 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700592
593 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400594 for shard_index, task_id in enumerate(task_ids):
595 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700596
597 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400598 shards_remaining = range(len(task_ids))
599 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700600 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700601 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700602 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700603 shard_index, result = results_channel.pull(
604 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700605 except threading_utils.TaskChannel.Timeout:
606 if print_status_updates:
607 print(
608 'Waiting for results from the following shards: %s' %
609 ', '.join(map(str, shards_remaining)))
610 sys.stdout.flush()
611 continue
612 except Exception:
613 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700614
615 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700616 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500618 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000619 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700620
Vadim Shtayurab450c602014-05-12 19:23:25 -0700621 # Yield back results to the caller.
622 assert shard_index in shards_remaining
623 shards_remaining.remove(shard_index)
624 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700625
maruel@chromium.org0437a732013-08-27 16:05:52 +0000626 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700627 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628 should_stop.set()
629
630
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400631def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000632 """Returns wrapped output for swarming task shard."""
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400633 if metadata.get('started_ts'):
634 pending = '%.1fs' % (
maruel50cb24b2015-09-10 12:04:16 -0700635 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
636 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400637 else:
638 pending = 'N/A'
639
maruelf33f3f62015-09-10 11:33:46 -0700640 if metadata.get('duration') is not None:
641 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400642 else:
643 duration = 'N/A'
644
maruelf33f3f62015-09-10 11:33:46 -0700645 if metadata.get('exit_code') is not None:
maruel9ba953f2015-09-10 14:01:58 -0700646 # Integers are encoded as string to not loose precision.
647 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400648 else:
649 exit_code = 'N/A'
650
651 bot_id = metadata.get('bot_id') or 'N/A'
652
maruelf33f3f62015-09-10 11:33:46 -0700653 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400654 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400655 tag_footer = (
656 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
657 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400658
659 tag_len = max(len(tag_header), len(tag_footer))
660 dash_pad = '+-%s-+\n' % ('-' * tag_len)
661 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
662 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
663
664 header = dash_pad + tag_header + dash_pad
665 footer = dash_pad + tag_footer + dash_pad[:-1]
maruelf33f3f62015-09-10 11:33:46 -0700666 output = metadata['output'].rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400667 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000668
669
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700670def collect(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400671 swarming, task_name, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400672 task_summary_json, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500673 """Retrieves results of a Swarming task."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700674 # Collect summary JSON and output files (if task_output_dir is not None).
675 output_collector = TaskOutputCollector(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400676 task_output_dir, task_name, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700677
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 seen_shards = set()
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400679 exit_code = 0
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400680 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700681 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400682 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400683 swarming, task_ids, timeout, None, print_status_updates,
684 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700685 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700686
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400687 # Default to failure if there was no process that even started.
maruelf33f3f62015-09-10 11:33:46 -0700688 shard_exit_code = metadata.get('exit_code')
689 if shard_exit_code:
690 shard_exit_code = int(shard_exit_code)
maruel8db72b72015-09-02 13:28:11 -0700691 if shard_exit_code:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400692 exit_code = shard_exit_code
maruelf33f3f62015-09-10 11:33:46 -0700693 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700694
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700695 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400696 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400697 if len(seen_shards) < len(task_ids):
698 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700699 else:
maruelf33f3f62015-09-10 11:33:46 -0700700 print('%s: %s %s' % (
701 metadata.get('bot_id', 'N/A'),
702 metadata['task_id'],
703 shard_exit_code))
704 if metadata['output']:
705 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400706 if output:
707 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700708 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700709 summary = output_collector.finalize()
710 if task_summary_json:
maruelf33f3f62015-09-10 11:33:46 -0700711 # TODO(maruel): Make this optional.
712 for i in summary['shards']:
713 if i:
714 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700715 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700716
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400717 if decorate and total_duration:
718 print('Total duration: %.1fs' % total_duration)
719
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400720 if len(seen_shards) != len(task_ids):
721 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700722 print >> sys.stderr, ('Results from some shards are missing: %s' %
723 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700724 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700725
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400726 return exit_code
maruel@chromium.org0437a732013-08-27 16:05:52 +0000727
728
maruelf33f3f62015-09-10 11:33:46 -0700729### API management.
730
731
732class APIError(Exception):
733 pass
734
735
736def endpoints_api_discovery_apis(host):
737 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
738 the APIs exposed by a host.
739
740 https://developers.google.com/discovery/v1/reference/apis/list
741 """
742 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
743 if data is None:
744 raise APIError('Failed to discover APIs on %s' % host)
745 out = {}
746 for api in data['items']:
747 if api['id'] == 'discovery:v1':
748 continue
749 # URL is of the following form:
750 # url = host + (
751 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
752 api_data = net.url_read_json(api['discoveryRestUrl'])
753 if api_data is None:
754 raise APIError('Failed to discover %s on %s' % (api['id'], host))
755 out[api['id']] = api_data
756 return out
757
758
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500759### Commands.
760
761
762def abort_task(_swarming, _manifest):
763 """Given a task manifest that was triggered, aborts its execution."""
764 # TODO(vadimsh): No supported by the server yet.
765
766
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400767def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400768 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500769 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500770 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500771 dest='dimensions', metavar='FOO bar',
772 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500773 parser.add_option_group(parser.filter_group)
774
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400775
Vadim Shtayurab450c602014-05-12 19:23:25 -0700776def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400777 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700778 parser.sharding_group.add_option(
779 '--shards', type='int', default=1,
780 help='Number of shards to trigger and collect.')
781 parser.add_option_group(parser.sharding_group)
782
783
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400784def add_trigger_options(parser):
785 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500786 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400787 add_filter_options(parser)
788
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400789 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500790 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500791 '-s', '--isolated',
792 help='Hash of the .isolated to grab from the isolate server')
793 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500794 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700795 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500796 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500797 '--priority', type='int', default=100,
798 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500799 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500800 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400801 help='Display name of the task. Defaults to '
802 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
803 'isolated file is provided, if a hash is provided, it defaults to '
804 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400805 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400806 '--tags', action='append', default=[],
807 help='Tags to assign to the task.')
808 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500809 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400810 help='User associated with the task. Defaults to authenticated user on '
811 'the server.')
812 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400813 '--idempotent', action='store_true', default=False,
814 help='When set, the server will actively try to find a previous task '
815 'with the same parameter and return this result instead if possible')
816 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400817 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400818 help='Seconds to allow the task to be pending for a bot to run before '
819 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400820 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400821 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400822 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400823 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400824 '--hard-timeout', type='int', default=60*60,
825 help='Seconds to allow the task to complete.')
826 parser.task_group.add_option(
827 '--io-timeout', type='int', default=20*60,
828 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500829 parser.task_group.add_option(
830 '--raw-cmd', action='store_true', default=False,
831 help='When set, the command after -- is used as-is without run_isolated. '
832 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500833 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000834
835
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500836def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500837 """Processes trigger options and uploads files to isolate server if necessary.
838 """
839 options.dimensions = dict(options.dimensions)
840 options.env = dict(options.env)
841
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500842 if not options.dimensions:
843 parser.error('Please at least specify one --dimension')
844 if options.raw_cmd:
845 if not args:
846 parser.error(
847 'Arguments with --raw-cmd should be passed after -- as command '
848 'delimiter.')
849 if options.isolate_server:
850 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
851
852 command = args
853 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500854 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500855 options.user,
856 '_'.join(
857 '%s=%s' % (k, v)
858 for k, v in sorted(options.dimensions.iteritems())))
maruelf33f3f62015-09-10 11:33:46 -0700859 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500860 else:
861 isolateserver.process_isolate_server_options(parser, options, False)
862 try:
maruelf33f3f62015-09-10 11:33:46 -0700863 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500864 except ValueError as e:
865 parser.error(str(e))
866
867 return TaskRequest(
maruelf33f3f62015-09-10 11:33:46 -0700868 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500869 dimensions=options.dimensions,
870 env=options.env,
871 expiration=options.expiration,
maruelf33f3f62015-09-10 11:33:46 -0700872 extra_args=command if inputs_ref else None,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500873 hard_timeout=options.hard_timeout,
874 idempotent=options.idempotent,
maruelf33f3f62015-09-10 11:33:46 -0700875 inputs_ref=inputs_ref,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500876 io_timeout=options.io_timeout,
877 name=options.task_name,
878 priority=options.priority,
879 tags=options.tags,
880 user=options.user,
881 verbose=options.verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000882
883
884def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500885 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000886 '-t', '--timeout',
887 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400888 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000889 help='Timeout to wait for result, set to 0 for no timeout; default: '
890 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500891 parser.group_logging.add_option(
892 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700893 parser.group_logging.add_option(
894 '--print-status-updates', action='store_true',
895 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400896 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700897 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700898 '--task-summary-json',
899 metavar='FILE',
900 help='Dump a summary of task results to this file as json. It contains '
901 'only shards statuses as know to server directly. Any output files '
902 'emitted by the task can be collected by using --task-output-dir')
903 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700904 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700905 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700906 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700907 'directory contains per-shard directory with output files produced '
908 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700909 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000910
911
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400912@subcommand.usage('bots...')
913def CMDbot_delete(parser, args):
914 """Forcibly deletes bots from the Swarming server."""
915 parser.add_option(
916 '-f', '--force', action='store_true',
917 help='Do not prompt for confirmation')
918 options, args = parser.parse_args(args)
919 if not args:
920 parser.error('Please specific bots to delete')
921
922 bots = sorted(args)
923 if not options.force:
924 print('Delete the following bots?')
925 for bot in bots:
926 print(' %s' % bot)
927 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
928 print('Goodbye.')
929 return 1
930
931 result = 0
932 for bot in bots:
maruelf33f3f62015-09-10 11:33:46 -0700933 url = '%s/_ah/api/swarming/v1/bot/%s' % (options.swarming, bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400934 if net.url_read_json(url, method='DELETE') is None:
935 print('Deleting %s failed' % bot)
936 result = 1
937 return result
938
939
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400940def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400941 """Returns information about the bots connected to the Swarming server."""
942 add_filter_options(parser)
943 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400944 '--dead-only', action='store_true',
945 help='Only print dead bots, useful to reap them and reimage broken bots')
946 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400947 '-k', '--keep-dead', action='store_true',
948 help='Do not filter out dead bots')
949 parser.filter_group.add_option(
950 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400951 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400952 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400953
954 if options.keep_dead and options.dead_only:
955 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700956
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400957 bots = []
958 cursor = None
959 limit = 250
960 # Iterate via cursors.
maruelf33f3f62015-09-10 11:33:46 -0700961 base_url = (
962 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400963 while True:
964 url = base_url
965 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400966 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400967 data = net.url_read_json(url)
968 if data is None:
969 print >> sys.stderr, 'Failed to access %s' % options.swarming
970 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400971 bots.extend(data['items'])
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400972 cursor = data['cursor']
973 if not cursor:
974 break
975
maruelf33f3f62015-09-10 11:33:46 -0700976 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400977 if options.dead_only:
maruelf33f3f62015-09-10 11:33:46 -0700978 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400979 continue
maruelf33f3f62015-09-10 11:33:46 -0700980 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400981 continue
982
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400983 # If the user requested to filter on dimensions, ensure the bot has all the
984 # dimensions requested.
maruelf33f3f62015-09-10 11:33:46 -0700985 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400986 for key, value in options.dimensions:
987 if key not in dimensions:
988 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400989 # A bot can have multiple value for a key, for example,
990 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
991 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400992 if isinstance(dimensions[key], list):
993 if value not in dimensions[key]:
994 break
995 else:
996 if value != dimensions[key]:
997 break
998 else:
maruelf33f3f62015-09-10 11:33:46 -0700999 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001000 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001001 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001002 if bot.get('task_id'):
1003 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001004 return 0
1005
1006
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001007@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001008def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001009 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001010
1011 The result can be in multiple part if the execution was sharded. It can
1012 potentially have retries.
1013 """
1014 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001015 parser.add_option(
1016 '-j', '--json',
1017 help='Load the task ids from .json as saved by trigger --dump-json')
maruelf33f3f62015-09-10 11:33:46 -07001018 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001019 if not args and not options.json:
1020 parser.error('Must specify at least one task id or --json.')
1021 if args and options.json:
1022 parser.error('Only use one of task id or --json.')
1023
1024 if options.json:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001025 try:
1026 with open(options.json) as f:
1027 tasks = sorted(
1028 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1029 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001030 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001031 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001032 else:
1033 valid = frozenset('0123456789abcdef')
1034 if any(not valid.issuperset(task_id) for task_id in args):
1035 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001036
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001037 try:
1038 return collect(
1039 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001040 None,
1041 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001042 options.timeout,
1043 options.decorate,
1044 options.print_status_updates,
1045 options.task_summary_json,
1046 options.task_output_dir)
1047 except Failure:
1048 on_error.report(None)
1049 return 1
1050
1051
maruelf33f3f62015-09-10 11:33:46 -07001052@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001053def CMDquery(parser, args):
maruelf33f3f62015-09-10 11:33:46 -07001054 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1055 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001056
1057 Examples:
maruelf33f3f62015-09-10 11:33:46 -07001058 Listing all bots:
1059 swarming.py query -S https://server-url bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001060
maruelf33f3f62015-09-10 11:33:46 -07001061 Listing last 10 tasks on a specific bot named 'swarm1':
1062 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001063 """
1064 CHUNK_SIZE = 250
1065
1066 parser.add_option(
1067 '-L', '--limit', type='int', default=200,
1068 help='Limit to enforce on limitless items (like number of tasks); '
1069 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001070 parser.add_option(
1071 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruelf33f3f62015-09-10 11:33:46 -07001072 parser.add_option(
1073 '--progress', action='store_true',
1074 help='Prints a dot at each request to show progress')
1075 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001076 if len(args) != 1:
maruelf33f3f62015-09-10 11:33:46 -07001077 parser.error(
1078 'Must specify only method name and optionally query args properly '
1079 'escaped.')
1080 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001081 url = base_url
1082 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001083 # Check check, change if not working out.
1084 merge_char = '&' if '?' in url else '?'
1085 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001086 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001087 if data is None:
maruelf33f3f62015-09-10 11:33:46 -07001088 # TODO(maruel): Do basic diagnostic.
1089 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001090 return 1
1091
1092 # Some items support cursors. Try to get automatically if cursors are needed
1093 # by looking at the 'cursor' items.
1094 while (
1095 data.get('cursor') and
1096 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001097 merge_char = '&' if '?' in base_url else '?'
1098 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001099 if options.limit:
1100 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruelf33f3f62015-09-10 11:33:46 -07001101 if options.progress:
1102 sys.stdout.write('.')
1103 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001104 new = net.url_read_json(url)
1105 if new is None:
maruelf33f3f62015-09-10 11:33:46 -07001106 if options.progress:
1107 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001108 print >> sys.stderr, 'Failed to access %s' % options.swarming
1109 return 1
1110 data['items'].extend(new['items'])
maruelf33f3f62015-09-10 11:33:46 -07001111 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001112
maruelf33f3f62015-09-10 11:33:46 -07001113 if options.progress:
1114 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001115 if options.limit and len(data.get('items', [])) > options.limit:
1116 data['items'] = data['items'][:options.limit]
1117 data.pop('cursor', None)
1118
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001119 if options.json:
maruelf33f3f62015-09-10 11:33:46 -07001120 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001121 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001122 try:
maruelf33f3f62015-09-10 11:33:46 -07001123 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001124 sys.stdout.write('\n')
1125 except IOError:
1126 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001127 return 0
1128
1129
maruelf33f3f62015-09-10 11:33:46 -07001130def CMDquery_list(parser, args):
1131 """Returns list of all the Swarming APIs that can be used with command
1132 'query'.
1133 """
1134 parser.add_option(
1135 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1136 options, args = parser.parse_args(args)
1137 if args:
1138 parser.error('No argument allowed.')
1139
1140 try:
1141 apis = endpoints_api_discovery_apis(options.swarming)
1142 except APIError as e:
1143 parser.error(str(e))
1144 if options.json:
1145 with open(options.json, 'wb') as f:
1146 json.dump(apis, f)
1147 else:
1148 help_url = (
1149 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1150 options.swarming)
1151 for api_id, api in sorted(apis.iteritems()):
1152 print api_id
1153 print ' ' + api['description']
1154 for resource_name, resource in sorted(api['resources'].iteritems()):
1155 print ''
1156 for method_name, method in sorted(resource['methods'].iteritems()):
1157 # Only list the GET ones.
1158 if method['httpMethod'] != 'GET':
1159 continue
1160 print '- %s.%s: %s' % (
1161 resource_name, method_name, method['path'])
1162 print ' ' + method['description']
1163 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1164 return 0
1165
1166
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001167@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001168def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001169 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001170
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001171 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001172 """
1173 add_trigger_options(parser)
1174 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001175 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001176 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001177 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001178 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001179 tasks = trigger_task_shards(
1180 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001181 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001182 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001183 'Failed to trigger %s(%s): %s' %
1184 (options.task_name, args[0], e.args[0]))
1185 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001186 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001187 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001188 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001189 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001190 task_ids = [
1191 t['task_id']
1192 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1193 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001194 try:
1195 return collect(
1196 options.swarming,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001197 options.task_name,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001198 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001199 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001200 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001201 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001202 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001203 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001204 except Failure:
1205 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001206 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001207
1208
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001209@subcommand.usage('task_id')
1210def CMDreproduce(parser, args):
1211 """Runs a task locally that was triggered on the server.
1212
1213 This running locally the same commands that have been run on the bot. The data
1214 downloaded will be in a subdirectory named 'work' of the current working
1215 directory.
1216 """
1217 options, args = parser.parse_args(args)
1218 if len(args) != 1:
1219 parser.error('Must specify exactly one task id.')
1220
maruelf33f3f62015-09-10 11:33:46 -07001221 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001222 request = net.url_read_json(url)
1223 if not request:
1224 print >> sys.stderr, 'Failed to retrieve request data for the task'
1225 return 1
1226
1227 if not os.path.isdir('work'):
1228 os.mkdir('work')
1229
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001230 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001231 env = None
1232 if properties['env']:
1233 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001234 logging.info('env: %r', properties['env'])
1235 env.update(
maruelf33f3f62015-09-10 11:33:46 -07001236 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1237 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001238
maruelf33f3f62015-09-10 11:33:46 -07001239 try:
1240 return subprocess.call(properties['command'], env=env, cwd='work')
1241 except OSError as e:
1242 print >> sys.stderr, 'Failed to run: %s' % ' '.join(properties['command'])
1243 print >> sys.stderr, str(e)
1244 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001245
1246
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001247@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001248def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001249 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001250
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001251 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001252 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001253
1254 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001255
1256 Passes all extra arguments provided after '--' as additional command line
1257 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001258 """
1259 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001260 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001261 parser.add_option(
1262 '--dump-json',
1263 metavar='FILE',
1264 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001265 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001266 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001267 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001268 tasks = trigger_task_shards(
1269 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001270 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001271 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001272 tasks_sorted = sorted(
1273 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001274 if options.dump_json:
1275 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001276 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001277 'tasks': tasks,
1278 }
1279 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001280 print('To collect results, use:')
1281 print(' swarming.py collect -S %s --json %s' %
1282 (options.swarming, options.dump_json))
1283 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001284 print('To collect results, use:')
1285 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001286 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1287 print('Or visit:')
1288 for t in tasks_sorted:
1289 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001290 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001291 except Failure:
1292 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001293 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001294
1295
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001296class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001297 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001298 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001299 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001300 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001301 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001302 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001303 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001304 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001305 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001306 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001307
1308 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001309 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001310 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001311 auth.process_auth_options(self, options)
1312 user = self._process_swarming(options)
1313 if hasattr(options, 'user') and not options.user:
1314 options.user = user
1315 return options, args
1316
1317 def _process_swarming(self, options):
1318 """Processes the --swarming option and aborts if not specified.
1319
1320 Returns the identity as determined by the server.
1321 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001322 if not options.swarming:
1323 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001324 try:
1325 options.swarming = net.fix_url(options.swarming)
1326 except ValueError as e:
1327 self.error('--swarming %s' % e)
1328 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001329 try:
1330 user = auth.ensure_logged_in(options.swarming)
1331 except ValueError as e:
1332 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001333 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001334
1335
1336def main(args):
1337 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001338 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001339
1340
1341if __name__ == '__main__':
1342 fix_encoding.fix_encoding()
1343 tools.disable_buffering()
1344 colorama.init()
1345 sys.exit(main(sys.argv[1:]))