blob: bc8136038b4d5cbf4b8ec739d414977c68e4df23 [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
Marc-Antoine Ruel02196392014-10-17 16:29:43 -04008__version__ = '0.5.3'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
10import hashlib
11import json
12import logging
13import os
Vadim Shtayurae3fbd102014-04-29 17:05:21 -070014import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import shutil
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -040016import StringIO
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import subprocess
18import sys
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -040022import urlparse
23import zipfile
maruel@chromium.org0437a732013-08-27 16:05:52 +000024
25from third_party import colorama
26from third_party.depot_tools import fix_encoding
27from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000028
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050029from utils import file_path
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel@chromium.org0437a732013-08-27 16:05:52 +000033from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000034from utils import tools
35from utils import zip_package
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040038import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000040import run_isolated
41
42
43ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
44TOOLS_PATH = os.path.join(ROOT_DIR, 'tools')
45
46
Vadim Shtayura86a2cef2014-04-18 11:13:39 -070047# How often to print status updates to stdout in 'collect'.
48STATUS_UPDATE_INTERVAL = 15 * 60.
49
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -040050
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -040051class State(object):
52 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +000053
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -040054 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
55 values are part of the API so if they change, the API changed.
56
57 It's in fact an enum. Values should be in decreasing order of importance.
58 """
59 RUNNING = 0x10
60 PENDING = 0x20
61 EXPIRED = 0x30
62 TIMED_OUT = 0x40
63 BOT_DIED = 0x50
64 CANCELED = 0x60
65 COMPLETED = 0x70
66
67 STATES = (RUNNING, PENDING, EXPIRED, TIMED_OUT, BOT_DIED, CANCELED, COMPLETED)
68 STATES_RUNNING = (RUNNING, PENDING)
69 STATES_NOT_RUNNING = (EXPIRED, TIMED_OUT, BOT_DIED, CANCELED, COMPLETED)
70 STATES_DONE = (TIMED_OUT, COMPLETED)
71 STATES_ABANDONED = (EXPIRED, BOT_DIED, CANCELED)
72
73 _NAMES = {
74 RUNNING: 'Running',
75 PENDING: 'Pending',
76 EXPIRED: 'Expired',
77 TIMED_OUT: 'Execution timed out',
78 BOT_DIED: 'Bot died',
79 CANCELED: 'User canceled',
80 COMPLETED: 'Completed',
81 }
82
83 @classmethod
84 def to_string(cls, state):
85 """Returns a user-readable string representing a State."""
86 if state not in cls._NAMES:
87 raise ValueError('Invalid state %s' % state)
88 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +000089
90
maruel@chromium.org0437a732013-08-27 16:05:52 +000091class Failure(Exception):
92 """Generic failure."""
93 pass
94
95
Vadim Shtayurae3fbd102014-04-29 17:05:21 -070096class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -070097 """Assembles task execution summary (for --task-summary-json output).
98
99 Optionally fetches task outputs from isolate server to local disk (used when
100 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700101
102 This object is shared among multiple threads running 'retrieve_results'
103 function, in particular they call 'process_shard_result' method in parallel.
104 """
105
106 def __init__(self, task_output_dir, task_name, shard_count):
107 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
108
109 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700110 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700111 task_name: name of the swarming task results belong to.
112 shard_count: expected number of task shards.
113 """
114 self.task_output_dir = task_output_dir
115 self.task_name = task_name
116 self.shard_count = shard_count
117
118 self._lock = threading.Lock()
119 self._per_shard_results = {}
120 self._storage = None
121
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700122 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700123 os.makedirs(self.task_output_dir)
124
Vadim Shtayurab450c602014-05-12 19:23:25 -0700125 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700126 """Stores results of a single task shard, fetches output files if necessary.
127
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400128 Modifies |result| in place.
129
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700130 Called concurrently from multiple threads.
131 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700132 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700133 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700134 if shard_index < 0 or shard_index >= self.shard_count:
135 logging.warning(
136 'Shard index %d is outside of expected range: [0; %d]',
137 shard_index, self.shard_count - 1)
138 return
139
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400140 assert not 'isolated_out' in result
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400141 result['isolated_out'] = None
142 for output in result['outputs']:
143 isolated_files_location = extract_output_files_location(output)
144 if isolated_files_location:
145 if result['isolated_out']:
146 raise ValueError('Unexpected two task with output')
147 result['isolated_out'] = isolated_files_location
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400148
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700149 # Store result dict of that shard, ignore results we've already seen.
150 with self._lock:
151 if shard_index in self._per_shard_results:
152 logging.warning('Ignoring duplicate shard index %d', shard_index)
153 return
154 self._per_shard_results[shard_index] = result
155
156 # Fetch output files if necessary.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400157 if self.task_output_dir and result['isolated_out']:
158 storage = self._get_storage(
159 result['isolated_out']['server'],
160 result['isolated_out']['namespace'])
161 if storage:
162 # Output files are supposed to be small and they are not reused across
163 # tasks. So use MemoryCache for them instead of on-disk cache. Make
164 # files writable, so that calling script can delete them.
165 isolateserver.fetch_isolated(
166 result['isolated_out']['hash'],
167 storage,
168 isolateserver.MemoryCache(file_mode_mask=0700),
169 os.path.join(self.task_output_dir, str(shard_index)),
170 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700171
172 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700173 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700174 with self._lock:
175 # Write an array of shard results with None for missing shards.
176 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700177 'shards': [
178 self._per_shard_results.get(i) for i in xrange(self.shard_count)
179 ],
180 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700181 # Write summary.json to task_output_dir as well.
182 if self.task_output_dir:
183 tools.write_json(
184 os.path.join(self.task_output_dir, 'summary.json'),
185 summary,
186 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700187 if self._storage:
188 self._storage.close()
189 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700190 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700191
192 def _get_storage(self, isolate_server, namespace):
193 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700194 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700195 with self._lock:
196 if not self._storage:
197 self._storage = isolateserver.get_storage(isolate_server, namespace)
198 else:
199 # Shards must all use exact same isolate server and namespace.
200 if self._storage.location != isolate_server:
201 logging.error(
202 'Task shards are using multiple isolate servers: %s and %s',
203 self._storage.location, isolate_server)
204 return None
205 if self._storage.namespace != namespace:
206 logging.error(
207 'Task shards are using multiple namespaces: %s and %s',
208 self._storage.namespace, namespace)
209 return None
210 return self._storage
211
212
maruel@chromium.org0437a732013-08-27 16:05:52 +0000213def now():
214 """Exists so it can be mocked easily."""
215 return time.time()
216
217
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700218def extract_output_files_location(task_log):
219 """Task log -> location of task output files to fetch.
220
221 TODO(vadimsh,maruel): Use side-channel to get this information.
222 See 'run_tha_test' in run_isolated.py for where the data is generated.
223
224 Returns:
225 Tuple (isolate server URL, namespace, isolated hash) on success.
226 None if information is missing or can not be parsed.
227 """
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400228 if not task_log:
229 return None
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700230 match = re.search(
231 r'\[run_isolated_out_hack\](.*)\[/run_isolated_out_hack\]',
232 task_log,
233 re.DOTALL)
234 if not match:
235 return None
236
237 def to_ascii(val):
238 if not isinstance(val, basestring):
239 raise ValueError()
240 return val.encode('ascii')
241
242 try:
243 data = json.loads(match.group(1))
244 if not isinstance(data, dict):
245 raise ValueError()
246 isolated_hash = to_ascii(data['hash'])
247 namespace = to_ascii(data['namespace'])
248 isolate_server = to_ascii(data['storage'])
249 if not file_path.is_url(isolate_server):
250 raise ValueError()
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400251 data = {
252 'hash': isolated_hash,
253 'namespace': namespace,
254 'server': isolate_server,
255 'view_url': '%s/browse?%s' % (isolate_server, urllib.urlencode(
256 [('namespace', namespace), ('hash', isolated_hash)])),
257 }
258 return data
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700259 except (KeyError, ValueError):
260 logging.warning(
261 'Unexpected value of run_isolated_out_hack: %s', match.group(1))
262 return None
263
264
265def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400266 base_url, shard_index, task_id, timeout, should_stop, output_collector):
267 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700268
Vadim Shtayurab450c602014-05-12 19:23:25 -0700269 Returns:
270 <result dict> on success.
271 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700272 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000273 assert isinstance(timeout, float), timeout
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400274 result_url = '%s/swarming/api/v1/client/task/%s' % (base_url, task_id)
275 output_url = '%s/swarming/api/v1/client/task/%s/output/all' % (
276 base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700277 started = now()
278 deadline = started + timeout if timeout else None
279 attempt = 0
280
281 while not should_stop.is_set():
282 attempt += 1
283
284 # Waiting for too long -> give up.
285 current_time = now()
286 if deadline and current_time >= deadline:
287 logging.error('retrieve_results(%s) timed out on attempt %d',
288 base_url, attempt)
289 return None
290
291 # Do not spin too fast. Spin faster at the beginning though.
292 # Start with 1 sec delay and for each 30 sec of waiting add another second
293 # of delay, until hitting 15 sec ceiling.
294 if attempt > 1:
295 max_delay = min(15, 1 + (current_time - started) / 30.0)
296 delay = min(max_delay, deadline - current_time) if deadline else max_delay
297 if delay > 0:
298 logging.debug('Waiting %.1f sec before retrying', delay)
299 should_stop.wait(delay)
300 if should_stop.is_set():
301 return None
302
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400303 # Disable internal retries in net.url_read_json, since we are doing retries
304 # ourselves.
305 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
306 result = net.url_read_json(result_url, retry_50x=False)
307 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400308 continue
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400309 if result['state'] in State.STATES_NOT_RUNNING:
310 out = net.url_read_json(output_url)
311 result['outputs'] = (out or {}).get('outputs', [])
312 if not result['outputs']:
313 logging.error('No output found for task %s', task_id)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700314 # Record the result, try to fetch attached output files (if any).
315 if output_collector:
316 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700317 output_collector.process_shard_result(shard_index, result)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700318 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000319
320
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700321def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400322 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
323 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500324 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000325
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700326 Duplicate shards are ignored. Shards are yielded in order of completion.
327 Timed out shards are NOT yielded at all. Caller can compare number of yielded
328 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000329
330 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500331 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 +0000332 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500333
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700334 output_collector is an optional instance of TaskOutputCollector that will be
335 used to fetch files produced by a task from isolate server to the local disk.
336
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500337 Yields:
338 (index, result). In particular, 'result' is defined as the
339 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000340 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000341 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400342 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700343 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700344 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700345
maruel@chromium.org0437a732013-08-27 16:05:52 +0000346 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
347 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700348 # Adds a task to the thread pool to call 'retrieve_results' and return
349 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400350 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700351 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000352 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400353 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
354 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700355
356 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400357 for shard_index, task_id in enumerate(task_ids):
358 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700359
360 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400361 shards_remaining = range(len(task_ids))
362 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700363 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700364 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700365 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700366 shard_index, result = results_channel.pull(
367 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700368 except threading_utils.TaskChannel.Timeout:
369 if print_status_updates:
370 print(
371 'Waiting for results from the following shards: %s' %
372 ', '.join(map(str, shards_remaining)))
373 sys.stdout.flush()
374 continue
375 except Exception:
376 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700377
378 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700379 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000380 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500381 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000382 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700383
Vadim Shtayurab450c602014-05-12 19:23:25 -0700384 # Yield back results to the caller.
385 assert shard_index in shards_remaining
386 shards_remaining.remove(shard_index)
387 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700388
maruel@chromium.org0437a732013-08-27 16:05:52 +0000389 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700390 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000391 should_stop.set()
392
393
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400394def get_data(isolate_server):
395 """Returns the 'data' section with all files necessary to bootstrap a task
396 execution.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000397 """
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400398 bundle = zip_package.ZipPackage(ROOT_DIR)
399 bundle.add_buffer(
400 'run_isolated.zip',
401 run_isolated.get_as_zip_package().zip_into_buffer(compress=False))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000402
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400403 # TODO(maruel): Get rid of this.
404 bundle_url = upload_zip_bundle(isolate_server, bundle)
405 return [(bundle_url, 'swarm_data.zip')]
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000406
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400407
408def get_run_isolated_commands(
409 isolate_server, namespace, isolated_hash, extra_args, profile, verbose):
410 """Returns the 'commands' to run an isolated task via run_isolated.py.
411
412 Returns:
413 commands list to be added to the request.
414 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000415 run_cmd = [
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400416 'python', 'run_isolated.zip',
417 '--hash', isolated_hash,
418 '--isolate-server', isolate_server,
419 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000420 ]
Marc-Antoine Ruel470bbe42014-10-03 11:47:30 -0400421 # TODO(maruel): Decide what to do with profile.
422 if verbose or profile:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000423 run_cmd.append('--verbose')
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700424 # Pass all extra args for run_isolated.py, it will pass them to the command.
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400425 if extra_args:
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700426 run_cmd.append('--')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400427 run_cmd.extend(extra_args)
Marc-Antoine Rueldbff01c2014-12-08 14:46:43 -0500428 return [run_cmd]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000429
430
Vadim Shtayurab450c602014-05-12 19:23:25 -0700431def setup_googletest(env, shards, index):
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500432 """Sets googletest specific environment variables."""
433 if shards > 1:
434 env = env.copy()
Vadim Shtayurab450c602014-05-12 19:23:25 -0700435 env['GTEST_SHARD_INDEX'] = str(index)
436 env['GTEST_TOTAL_SHARDS'] = str(shards)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500437 return env
438
439
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500440def archive(isolate_server, namespace, isolated, algo, verbose):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000441 """Archives a .isolated and all the dependencies on the CAC."""
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500442 logging.info('archive(%s, %s, %s)', isolate_server, namespace, isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000443 tempdir = None
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500444 if file_path.is_url(isolate_server):
445 command = 'archive'
446 flag = '--isolate-server'
447 else:
448 command = 'hashtable'
449 flag = '--outdir'
450
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500451 print('Archiving: %s' % isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000452 try:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000453 cmd = [
454 sys.executable,
455 os.path.join(ROOT_DIR, 'isolate.py'),
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500456 command,
457 flag, isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500458 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000459 '--isolated', isolated,
460 ]
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000461 cmd.extend(['--verbose'] * verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000462 logging.info(' '.join(cmd))
463 if subprocess.call(cmd, verbose):
464 return
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400465 return isolated_format.hash_file(isolated, algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000466 finally:
467 if tempdir:
468 shutil.rmtree(tempdir)
469
470
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400471def get_shard_task_name(task_name, index, shards):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700472 """Returns a task name to use for a single shard of a task."""
473 if shards == 1:
474 return task_name
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400475 return '%s:%s:%s' % (task_name, index, shards)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700476
477
478def upload_zip_bundle(isolate_server, bundle):
479 """Uploads a zip package to isolate storage and returns raw fetch URL.
480
481 Args:
482 isolate_server: URL of an isolate server.
483 bundle: instance of ZipPackage to upload.
484
485 Returns:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400486 URL to get the file from.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700487 """
488 # Swarming bot would need to be able to grab the file from the storage
489 # using raw HTTP GET. Use 'default' namespace so that the raw data returned
490 # to a bot is not zipped, since swarm_bot doesn't understand compressed
491 # data yet. This namespace have nothing to do with |namespace| passed to
492 # run_isolated.py that is used to store files for isolated task.
493 logging.info('Zipping up and uploading files...')
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400494 start_time = now()
495 isolate_item = isolateserver.BufferItem(
496 bundle.zip_into_buffer(), high_priority=True)
497 with isolateserver.get_storage(isolate_server, 'default') as storage:
498 uploaded = storage.upload_items([isolate_item])
499 bundle_url = storage.get_fetch_url(isolate_item)
500 elapsed = now() - start_time
Vadim Shtayurab450c602014-05-12 19:23:25 -0700501 if isolate_item in uploaded:
502 logging.info('Upload complete, time elapsed: %f', elapsed)
503 else:
504 logging.info('Zip file already on server, time elapsed: %f', elapsed)
505 return bundle_url
506
507
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400508def trigger_request(swarming, request, xsrf_token):
509 """Triggers a requests on the Swarming server and returns the json data.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700510
511 Returns:
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400512 {
513 'request': {
514 'created_ts': u'2010-01-02 03:04:05',
515 'name': ..
516 },
517 'task_id': '12300',
518 }
Vadim Shtayurab450c602014-05-12 19:23:25 -0700519 """
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400520 logging.info('Triggering: %s', request['name'])
521
522 headers = {'X-XSRF-Token': xsrf_token}
523 result = net.url_read_json(
524 swarming + '/swarming/api/v1/client/request',
525 data=request,
526 headers=headers)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700527 if not result:
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400528 on_error.report('Failed to trigger task %s' % request['name'])
529 return None
530 return result
Vadim Shtayurab450c602014-05-12 19:23:25 -0700531
532
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400533def abort_task(_swarming, _manifest):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700534 """Given a task manifest that was triggered, aborts its execution."""
535 # TODO(vadimsh): No supported by the server yet.
536
537
538def trigger_task_shards(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700539 swarming, isolate_server, namespace, isolated_hash, task_name, extra_args,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400540 shards, dimensions, env, expiration, io_timeout, hard_timeout, idempotent,
541 verbose, profile, priority, tags, user):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400542 """Triggers multiple subtasks of a sharded task.
543
544 Returns:
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700545 Dict with task details, returned to caller as part of --dump-json output.
546 None in case of failure.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400547 """
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400548 try:
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400549 data = get_data(isolate_server)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400550 except (IOError, OSError):
551 on_error.report('Failed to upload the zip file for task %s' % task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400552 return None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000553
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400554 def gen_request(env, index):
555 """Returns the json dict expected by the Swarming server for new request."""
556 return {
557 'name': get_shard_task_name(task_name, index, shards),
558 'priority': priority,
559 'properties': {
560 'commands': get_run_isolated_commands(
561 isolate_server, namespace, isolated_hash, extra_args, profile,
562 verbose),
563 'data': data,
564 'dimensions': dimensions,
565 'env': env,
566 'execution_timeout_secs': hard_timeout,
567 'io_timeout_secs': io_timeout,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400568 'idempotent': idempotent,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400569 },
570 'scheduling_expiration_secs': expiration,
571 'tags': tags,
572 'user': user,
573 }
maruel@chromium.org0437a732013-08-27 16:05:52 +0000574
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400575 requests = [
576 gen_request(setup_googletest(env, shards, index), index)
577 for index in xrange(shards)
578 ]
579
580 headers = {'X-XSRF-Token-Request': '1'}
581 response = net.url_read_json(
582 swarming + '/swarming/api/v1/client/handshake',
583 headers=headers,
584 data={})
585 if not response:
586 logging.error('Failed to handshake with server')
587 return None
588 logging.info('Connected to server version: %s', response['server_version'])
589 xsrf_token = response['xsrf_token']
590
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400591 tasks = {}
592 priority_warning = False
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400593 for index, request in enumerate(requests):
594 task = trigger_request(swarming, request, xsrf_token)
595 if not task:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700596 break
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400597 logging.info('Request result: %s', task)
598 priority = task['request']['priority']
599 if not priority_warning and priority != request['priority']:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400600 priority_warning = True
601 print >> sys.stderr, 'Priority was reset to %s' % priority
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400602 tasks[request['name']] = {
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700603 'shard_index': index,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400604 'task_id': task['task_id'],
605 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700606 }
Vadim Shtayurab450c602014-05-12 19:23:25 -0700607
608 # Some shards weren't triggered. Abort everything.
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400609 if len(tasks) != len(requests):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400610 if tasks:
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400611 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
612 len(tasks), len(requests))
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700613 for task_dict in tasks.itervalues():
614 abort_task(swarming, task_dict['task_id'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400615 return None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000616
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400617 return tasks
maruel@chromium.org0437a732013-08-27 16:05:52 +0000618
619
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500620def isolated_to_hash(isolate_server, namespace, arg, algo, verbose):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500621 """Archives a .isolated file if needed.
622
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500623 Returns the file hash to trigger and a bool specifying if it was a file (True)
624 or a hash (False).
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500625 """
626 if arg.endswith('.isolated'):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500627 file_hash = archive(isolate_server, namespace, arg, algo, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500628 if not file_hash:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400629 on_error.report('Archival failure %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500630 return None, True
631 return file_hash, True
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400632 elif isolated_format.is_valid_hash(arg, algo):
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500633 return arg, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500634 else:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400635 on_error.report('Invalid hash %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500636 return None, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500637
638
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639def trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500640 swarming,
641 isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500642 namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500643 file_hash_or_isolated,
644 task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700645 extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500646 shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500647 dimensions,
648 env,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400649 expiration,
650 io_timeout,
651 hard_timeout,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400652 idempotent,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000653 verbose,
654 profile,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400655 priority,
656 tags,
657 user):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400658 """Sends off the hash swarming task requests.
659
660 Returns:
661 tuple(dict(task_name: task_id), base task name). The dict of tasks is None
662 in case of failure.
663 """
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500664 file_hash, is_file = isolated_to_hash(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500665 isolate_server, namespace, file_hash_or_isolated, hashlib.sha1, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500666 if not file_hash:
Marc-Antoine Ruel47046d92014-12-08 14:28:11 -0500667 return None, ''
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500668 if not task_name:
669 # If a file name was passed, use its base name of the isolated hash.
670 # Otherwise, use user name as an approximation of a task name.
671 if is_file:
672 key = os.path.splitext(os.path.basename(file_hash_or_isolated))[0]
673 else:
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400674 key = user
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700675 task_name = '%s/%s/%s/%d' % (
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500676 key,
677 '_'.join('%s=%s' % (k, v) for k, v in sorted(dimensions.iteritems())),
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700678 file_hash,
679 now() * 1000)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500680
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400681 tasks = trigger_task_shards(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500682 swarming=swarming,
683 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500684 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500685 isolated_hash=file_hash,
686 task_name=task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700687 extra_args=extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500688 shards=shards,
689 dimensions=dimensions,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400690 expiration=expiration,
691 io_timeout=io_timeout,
692 hard_timeout=hard_timeout,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400693 idempotent=idempotent,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500694 env=env,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500695 verbose=verbose,
696 profile=profile,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400697 priority=priority,
698 tags=tags,
699 user=user)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400700 return tasks, task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +0000701
702
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400703def decorate_shard_output(
704 swarming, shard_index, result, shard_exit_code, shard_duration):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000705 """Returns wrapped output for swarming task shard."""
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400706 url = '%s/user/task/%s' % (swarming, result['id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400707 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel9b17dae2014-10-17 16:28:43 -0400708 tag_footer = 'End of shard %d Duration: %.1fs Bot: %s Exit code %s' % (
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400709 shard_index, shard_duration, result['bot_id'], shard_exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400710
711 tag_len = max(len(tag_header), len(tag_footer))
712 dash_pad = '+-%s-+\n' % ('-' * tag_len)
713 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
714 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
715
716 header = dash_pad + tag_header + dash_pad
717 footer = dash_pad + tag_footer + dash_pad[:-1]
718 output = '\n'.join(o for o in result['outputs'] if o).rstrip() + '\n'
719 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000720
721
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700722def collect(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400723 swarming, task_name, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400724 task_summary_json, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500725 """Retrieves results of a Swarming task."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700726 # Collect summary JSON and output files (if task_output_dir is not None).
727 output_collector = TaskOutputCollector(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400728 task_output_dir, task_name, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700729
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700730 seen_shards = set()
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400731 exit_code = 0
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400732 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700733 try:
734 for index, output in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400735 swarming, task_ids, timeout, None, print_status_updates,
736 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700737 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700738
Marc-Antoine Ruel9b17dae2014-10-17 16:28:43 -0400739 # Grab first non-zero exit code as an overall shard exit code. Default to
740 # failure if there was no process that even started.
741 shard_exit_code = 1
742 shard_exit_codes = sorted(output['exit_codes'], key=lambda x: not x)
743 if shard_exit_codes:
744 shard_exit_code = shard_exit_codes[0]
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400745 if shard_exit_code:
746 exit_code = shard_exit_code
Vadim Shtayura473455a2014-05-14 15:22:35 -0700747
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400748 shard_duration = sum(i for i in output['durations'] if i)
749 total_duration += shard_duration
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700750 if decorate:
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400751 print(decorate_shard_output(
752 swarming, index, output, shard_exit_code, shard_duration))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400753 if len(seen_shards) < len(task_ids):
754 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700755 else:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400756 print('%s: %s %d' % (output['bot_id'], output['id'], shard_exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400757 for output in output['outputs']:
758 if not output:
759 continue
760 output = output.rstrip()
761 if output:
762 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700763 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700764 summary = output_collector.finalize()
765 if task_summary_json:
766 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700767
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400768 if decorate and total_duration:
769 print('Total duration: %.1fs' % total_duration)
770
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400771 if len(seen_shards) != len(task_ids):
772 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700773 print >> sys.stderr, ('Results from some shards are missing: %s' %
774 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700775 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700776
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400777 return exit_code
maruel@chromium.org0437a732013-08-27 16:05:52 +0000778
779
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400780def add_filter_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500781 parser.filter_group = tools.optparse.OptionGroup(parser, 'Filtering slaves')
782 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500783 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500784 dest='dimensions', metavar='FOO bar',
785 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500786 parser.add_option_group(parser.filter_group)
787
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400788
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400789def process_filter_options(parser, options):
790 options.dimensions = dict(options.dimensions)
791 if not options.dimensions:
792 parser.error('Please at least specify one --dimension')
793
794
Vadim Shtayurab450c602014-05-12 19:23:25 -0700795def add_sharding_options(parser):
796 parser.sharding_group = tools.optparse.OptionGroup(parser, 'Sharding options')
797 parser.sharding_group.add_option(
798 '--shards', type='int', default=1,
799 help='Number of shards to trigger and collect.')
800 parser.add_option_group(parser.sharding_group)
801
802
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400803def add_trigger_options(parser):
804 """Adds all options to trigger a task on Swarming."""
805 isolateserver.add_isolate_server_options(parser, True)
806 add_filter_options(parser)
807
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500808 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
809 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500810 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700811 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500812 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500813 '--priority', type='int', default=100,
814 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500815 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500816 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400817 help='Display name of the task. Defaults to '
818 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
819 'isolated file is provided, if a hash is provided, it defaults to '
820 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400821 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400822 '--tags', action='append', default=[],
823 help='Tags to assign to the task.')
824 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500825 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400826 help='User associated with the task. Defaults to authenticated user on '
827 'the server.')
828 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400829 '--idempotent', action='store_true', default=False,
830 help='When set, the server will actively try to find a previous task '
831 'with the same parameter and return this result instead if possible')
832 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400833 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400834 help='Seconds to allow the task to be pending for a bot to run before '
835 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400836 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400837 '--deadline', type='int', dest='expiration',
838 help=tools.optparse.SUPPRESS_HELP)
839 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400840 '--hard-timeout', type='int', default=60*60,
841 help='Seconds to allow the task to complete.')
842 parser.task_group.add_option(
843 '--io-timeout', type='int', default=20*60,
844 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500845 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500846 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500847 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000848 '--profile', action='store_true',
849 default=bool(os.environ.get('ISOLATE_DEBUG')),
850 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000851
852
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500853def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500854 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500855 if len(args) != 1:
856 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400857 process_filter_options(parser, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000858
859
860def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500861 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000862 '-t', '--timeout',
863 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400864 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000865 help='Timeout to wait for result, set to 0 for no timeout; default: '
866 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500867 parser.group_logging.add_option(
868 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700869 parser.group_logging.add_option(
870 '--print-status-updates', action='store_true',
871 help='Print periodic status updates')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700872 parser.task_output_group = tools.optparse.OptionGroup(parser, 'Task output')
873 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700874 '--task-summary-json',
875 metavar='FILE',
876 help='Dump a summary of task results to this file as json. It contains '
877 'only shards statuses as know to server directly. Any output files '
878 'emitted by the task can be collected by using --task-output-dir')
879 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700880 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700881 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700882 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700883 'directory contains per-shard directory with output files produced '
884 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700885 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000886
887
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700888def extract_isolated_command_extra_args(args):
889 try:
890 index = args.index('--')
891 except ValueError:
892 return (args, [])
893 return (args[:index], args[index+1:])
894
895
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400896def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400897 """Returns information about the bots connected to the Swarming server."""
898 add_filter_options(parser)
899 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400900 '--dead-only', action='store_true',
901 help='Only print dead bots, useful to reap them and reimage broken bots')
902 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400903 '-k', '--keep-dead', action='store_true',
904 help='Do not filter out dead bots')
905 parser.filter_group.add_option(
906 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400907 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400908 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400909
910 if options.keep_dead and options.dead_only:
911 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700912
913 auth.ensure_logged_in(options.swarming)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400914
915 bots = []
916 cursor = None
917 limit = 250
918 # Iterate via cursors.
919 base_url = options.swarming + '/swarming/api/v1/client/bots?limit=%d' % limit
920 while True:
921 url = base_url
922 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400923 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400924 data = net.url_read_json(url)
925 if data is None:
926 print >> sys.stderr, 'Failed to access %s' % options.swarming
927 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400928 bots.extend(data['items'])
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400929 cursor = data['cursor']
930 if not cursor:
931 break
932
933 for bot in natsort.natsorted(bots, key=lambda x: x['id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400934 if options.dead_only:
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400935 if not bot['is_dead']:
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400936 continue
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400937 elif not options.keep_dead and bot['is_dead']:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400938 continue
939
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400940 # If the user requested to filter on dimensions, ensure the bot has all the
941 # dimensions requested.
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400942 dimensions = bot['dimensions']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400943 for key, value in options.dimensions:
944 if key not in dimensions:
945 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400946 # A bot can have multiple value for a key, for example,
947 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
948 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400949 if isinstance(dimensions[key], list):
950 if value not in dimensions[key]:
951 break
952 else:
953 if value != dimensions[key]:
954 break
955 else:
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400956 print bot['id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400957 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -0400958 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -0500959 if bot.get('task_id'):
960 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400961 return 0
962
963
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400964@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400965def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400966 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400967
968 The result can be in multiple part if the execution was sharded. It can
969 potentially have retries.
970 """
971 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400972 parser.add_option(
973 '-j', '--json',
974 help='Load the task ids from .json as saved by trigger --dump-json')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400975 (options, args) = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400976 if not args and not options.json:
977 parser.error('Must specify at least one task id or --json.')
978 if args and options.json:
979 parser.error('Only use one of task id or --json.')
980
981 if options.json:
982 with open(options.json) as f:
983 tasks = sorted(
984 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
985 args = [t['task_id'] for t in tasks]
986 else:
987 valid = frozenset('0123456789abcdef')
988 if any(not valid.issuperset(task_id) for task_id in args):
989 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400990
991 auth.ensure_logged_in(options.swarming)
992 try:
993 return collect(
994 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400995 None,
996 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400997 options.timeout,
998 options.decorate,
999 options.print_status_updates,
1000 options.task_summary_json,
1001 options.task_output_dir)
1002 except Failure:
1003 on_error.report(None)
1004 return 1
1005
1006
1007@subcommand.usage('[resource name]')
1008def CMDquery(parser, args):
1009 """Returns raw JSON information via an URL endpoint. Use 'list' to gather the
1010 list of valid values from the server.
1011
1012 Examples:
1013 Printing the list of known URLs:
1014 swarming.py query -S https://server-url list
1015
1016 Listing last 50 tasks on a specific bot named 'swarm1'
1017 swarming.py query -S https://server-url --limit 50 bot/swarm1/tasks
1018 """
1019 CHUNK_SIZE = 250
1020
1021 parser.add_option(
1022 '-L', '--limit', type='int', default=200,
1023 help='Limit to enforce on limitless items (like number of tasks); '
1024 'default=%default')
1025 (options, args) = parser.parse_args(args)
1026 if len(args) != 1:
1027 parser.error('Must specify only one resource name.')
1028
1029 auth.ensure_logged_in(options.swarming)
1030
1031 base_url = options.swarming + '/swarming/api/v1/client/' + args[0]
1032 url = base_url
1033 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001034 # Check check, change if not working out.
1035 merge_char = '&' if '?' in url else '?'
1036 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001037 data = net.url_read_json(url)
1038 if data is None:
1039 print >> sys.stderr, 'Failed to access %s' % options.swarming
1040 return 1
1041
1042 # Some items support cursors. Try to get automatically if cursors are needed
1043 # by looking at the 'cursor' items.
1044 while (
1045 data.get('cursor') and
1046 (not options.limit or len(data['items']) < options.limit)):
1047 url = base_url + '?cursor=%s' % urllib.quote(data['cursor'])
1048 if options.limit:
1049 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
1050 new = net.url_read_json(url)
1051 if new is None:
1052 print >> sys.stderr, 'Failed to access %s' % options.swarming
1053 return 1
1054 data['items'].extend(new['items'])
1055 data['cursor'] = new['cursor']
1056
1057 if options.limit and len(data.get('items', [])) > options.limit:
1058 data['items'] = data['items'][:options.limit]
1059 data.pop('cursor', None)
1060
1061 json.dump(data, sys.stdout, indent=2, sort_keys=True)
1062 sys.stdout.write('\n')
1063 return 0
1064
1065
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001066@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001067def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001068 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001069
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001070 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001071 """
1072 add_trigger_options(parser)
1073 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001074 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001075 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001076 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001077 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001078
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001079 user = auth.ensure_logged_in(options.swarming)
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001080 if file_path.is_url(options.isolate_server):
1081 auth.ensure_logged_in(options.isolate_server)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001082
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -05001083 options.user = options.user or user
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001084 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001085 tasks, task_name = trigger(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001086 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001087 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001088 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001089 file_hash_or_isolated=args[0],
1090 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001091 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001092 shards=options.shards,
1093 dimensions=options.dimensions,
1094 env=dict(options.env),
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001095 expiration=options.expiration,
1096 io_timeout=options.io_timeout,
1097 hard_timeout=options.hard_timeout,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -04001098 idempotent=options.idempotent,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001099 verbose=options.verbose,
1100 profile=options.profile,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001101 priority=options.priority,
1102 tags=options.tags,
1103 user=options.user)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001104 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001105 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001106 'Failed to trigger %s(%s): %s' %
1107 (options.task_name, args[0], e.args[0]))
1108 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001109 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001110 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001111 return 1
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001112 if task_name != options.task_name:
1113 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001114 task_ids = [
1115 t['task_id']
1116 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1117 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001118 try:
1119 return collect(
1120 options.swarming,
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001121 task_name,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001122 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001123 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001124 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001125 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001126 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001127 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001128 except Failure:
1129 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001130 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001131
1132
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001133@subcommand.usage('task_id')
1134def CMDreproduce(parser, args):
1135 """Runs a task locally that was triggered on the server.
1136
1137 This running locally the same commands that have been run on the bot. The data
1138 downloaded will be in a subdirectory named 'work' of the current working
1139 directory.
1140 """
1141 options, args = parser.parse_args(args)
1142 if len(args) != 1:
1143 parser.error('Must specify exactly one task id.')
1144
1145 auth.ensure_logged_in(options.swarming)
1146 url = options.swarming + '/swarming/api/v1/client/task/%s/request' % args[0]
1147 request = net.url_read_json(url)
1148 if not request:
1149 print >> sys.stderr, 'Failed to retrieve request data for the task'
1150 return 1
1151
1152 if not os.path.isdir('work'):
1153 os.mkdir('work')
1154
1155 swarming_host = urlparse.urlparse(options.swarming).netloc
1156 properties = request['properties']
1157 for data_url, _ in properties['data']:
1158 assert data_url.startswith('https://'), data_url
1159 data_host = urlparse.urlparse(data_url).netloc
1160 if data_host != swarming_host:
1161 auth.ensure_logged_in('https://' + data_host)
1162
1163 content = net.url_read(data_url)
1164 if content is None:
1165 print >> sys.stderr, 'Failed to download %s' % data_url
1166 return 1
1167 with zipfile.ZipFile(StringIO.StringIO(content)) as zip_file:
1168 zip_file.extractall('work')
1169
1170 env = None
1171 if properties['env']:
1172 env = os.environ.copy()
1173 env.update(properties['env'])
1174
1175 exit_code = 0
1176 for cmd in properties['commands']:
1177 try:
1178 c = subprocess.call(cmd, env=env, cwd='work')
1179 except OSError as e:
1180 print >> sys.stderr, 'Failed to run: %s' % ' '.join(cmd)
1181 print >> sys.stderr, str(e)
1182 c = 1
1183 if not exit_code:
1184 exit_code = c
1185 return exit_code
1186
1187
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001188@subcommand.usage("(hash|isolated) [-- extra_args]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001189def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001190 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001191
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001192 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001193 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001194
1195 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001196
1197 Passes all extra arguments provided after '--' as additional command line
1198 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001199 """
1200 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001201 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001202 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001203 parser.add_option(
1204 '--dump-json',
1205 metavar='FILE',
1206 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001207 options, args = parser.parse_args(args)
1208 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001209
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001210 user = auth.ensure_logged_in(options.swarming)
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001211 if file_path.is_url(options.isolate_server):
1212 auth.ensure_logged_in(options.isolate_server)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001213
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -05001214 options.user = options.user or user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001215 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001216 tasks, task_name = trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001217 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001218 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001219 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001220 file_hash_or_isolated=args[0],
1221 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001222 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001223 shards=options.shards,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001224 dimensions=options.dimensions,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -05001225 env=dict(options.env),
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001226 expiration=options.expiration,
1227 io_timeout=options.io_timeout,
1228 hard_timeout=options.hard_timeout,
Marc-Antoine Ruel02196392014-10-17 16:29:43 -04001229 idempotent=options.idempotent,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001230 verbose=options.verbose,
1231 profile=options.profile,
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001232 priority=options.priority,
1233 tags=options.tags,
1234 user=options.user)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001235 if tasks:
1236 if task_name != options.task_name:
1237 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001238 tasks_sorted = sorted(
1239 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001240 if options.dump_json:
1241 data = {
1242 'base_task_name': task_name,
1243 'tasks': tasks,
1244 }
1245 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001246 print('To collect results, use:')
1247 print(' swarming.py collect -S %s --json %s' %
1248 (options.swarming, options.dump_json))
1249 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001250 print('To collect results, use:')
1251 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001252 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1253 print('Or visit:')
1254 for t in tasks_sorted:
1255 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001256 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001257 except Failure:
1258 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001259 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001260
1261
1262class OptionParserSwarming(tools.OptionParserWithLogging):
1263 def __init__(self, **kwargs):
1264 tools.OptionParserWithLogging.__init__(
1265 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001266 self.server_group = tools.optparse.OptionGroup(self, 'Server')
1267 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001268 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001269 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001270 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001271 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001272 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001273
1274 def parse_args(self, *args, **kwargs):
1275 options, args = tools.OptionParserWithLogging.parse_args(
1276 self, *args, **kwargs)
1277 options.swarming = options.swarming.rstrip('/')
1278 if not options.swarming:
1279 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001280 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001281 return options, args
1282
1283
1284def main(args):
1285 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001286 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001287
1288
1289if __name__ == '__main__':
1290 fix_encoding.fix_encoding()
1291 tools.disable_buffering()
1292 colorama.init()
1293 sys.exit(main(sys.argv[1:]))