blob: 33a2be4b101529e01e45da4833af04d3f28bde08 [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 Ruele4dcbb82014-10-01 09:30:56 -04008__version__ = '0.5.1'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -050010import getpass
maruel@chromium.org0437a732013-08-27 16:05:52 +000011import hashlib
12import json
13import logging
14import os
Vadim Shtayurae3fbd102014-04-29 17:05:21 -070015import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import shutil
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
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
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
33from utils import zip_package
maruel@chromium.org0437a732013-08-27 16:05:52 +000034
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080035import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040036import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000037import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000038import run_isolated
39
40
41ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
42TOOLS_PATH = os.path.join(ROOT_DIR, 'tools')
43
44
maruel@chromium.org0437a732013-08-27 16:05:52 +000045# The default time to wait for a shard to finish running.
csharp@chromium.org24758492013-08-28 19:10:54 +000046DEFAULT_SHARD_WAIT_TIME = 80 * 60.
maruel@chromium.org0437a732013-08-27 16:05:52 +000047
Vadim Shtayura86a2cef2014-04-18 11:13:39 -070048# How often to print status updates to stdout in 'collect'.
49STATUS_UPDATE_INTERVAL = 15 * 60.
50
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
96class Manifest(object):
Vadim Shtayurab450c602014-05-12 19:23:25 -070097 """Represents a Swarming task manifest."""
maruel@chromium.org0437a732013-08-27 16:05:52 +000098
maruel@chromium.org0437a732013-08-27 16:05:52 +000099 def __init__(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700100 self, isolate_server, namespace, isolated_hash, task_name, extra_args,
Marc-Antoine Ruelaea50652014-06-12 14:23:48 -0400101 env, dimensions, deadline, verbose, profile,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700102 priority):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000103 """Populates a manifest object.
104 Args:
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500105 isolate_server - isolate server url.
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500106 namespace - isolate server namespace to use.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700107 isolated_hash - the manifest's sha-1 that the slave is going to fetch.
108 task_name - the name to give the task request.
109 extra_args - additional arguments to pass to isolated command.
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -0500110 env - environment variables to set.
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500111 dimensions - dimensions to filter the task on.
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400112 deadline - maximum pending time before this task expires.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000113 verbose - if True, have the slave print more details.
114 profile - if True, have the slave print more timing data.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000115 priority - int between 0 and 1000, lower the higher priority.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000116 """
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500117 self.isolate_server = isolate_server
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500118 self.namespace = namespace
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000119 self.isolated_hash = isolated_hash
Vadim Shtayurab450c602014-05-12 19:23:25 -0700120 self.task_name = task_name
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700121 self.extra_args = tuple(extra_args or [])
Vadim Shtayurab450c602014-05-12 19:23:25 -0700122 self.env = env.copy()
123 self.dimensions = dimensions.copy()
Vadim Shtayurab450c602014-05-12 19:23:25 -0700124 self.deadline = deadline
maruel@chromium.org0437a732013-08-27 16:05:52 +0000125 self.verbose = bool(verbose)
126 self.profile = bool(profile)
127 self.priority = priority
maruel@chromium.org0437a732013-08-27 16:05:52 +0000128 self._tasks = []
Vadim Shtayurab450c602014-05-12 19:23:25 -0700129 self._files = []
maruel@chromium.org0437a732013-08-27 16:05:52 +0000130
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400131 def add_task(self, task_name, actions, time_out=2*60*60):
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500132 """Appends a new task as a TestObject to the swarming manifest file.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500133
134 Tasks cannot be added once the manifest was uploaded.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500135
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400136 By default, command will be killed after 2 hours of execution.
137
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500138 See TestObject in services/swarming/src/common/test_request_message.py for
139 the valid format.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500140 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000141 self._tasks.append(
142 {
143 'action': actions,
144 'decorate_output': self.verbose,
145 'test_name': task_name,
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400146 'hard_time_out': time_out,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000147 })
148
Vadim Shtayurab450c602014-05-12 19:23:25 -0700149 def add_bundled_file(self, file_name, file_url):
150 """Appends a file to the manifest.
151
152 File will be downloaded and extracted by the swarm bot before launching the
153 task.
154 """
155 self._files.append([file_url, file_name])
156
maruel@chromium.org0437a732013-08-27 16:05:52 +0000157 def to_json(self):
158 """Exports the current configuration into a swarm-readable manifest file.
159
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500160 The actual serialization format is defined as a TestCase object as described
161 in services/swarming/src/common/test_request_message.py
maruel@chromium.org0437a732013-08-27 16:05:52 +0000162 """
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500163 request = {
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500164 'cleanup': 'root',
maruel@chromium.org0437a732013-08-27 16:05:52 +0000165 'configurations': [
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500166 # Is a TestConfiguration.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000167 {
Marc-Antoine Ruel5d799192013-11-06 15:20:39 -0500168 'config_name': 'isolated',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700169 'deadline_to_run': self.deadline,
170 'dimensions': self.dimensions,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500171 'priority': self.priority,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000172 },
173 ],
Vadim Shtayurab450c602014-05-12 19:23:25 -0700174 'data': self._files,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700175 'env_vars': self.env,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700176 'test_case_name': self.task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500177 'tests': self._tasks,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000178 }
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500179 return json.dumps(request, sort_keys=True, separators=(',',':'))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000180
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500181
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700182class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700183 """Assembles task execution summary (for --task-summary-json output).
184
185 Optionally fetches task outputs from isolate server to local disk (used when
186 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700187
188 This object is shared among multiple threads running 'retrieve_results'
189 function, in particular they call 'process_shard_result' method in parallel.
190 """
191
192 def __init__(self, task_output_dir, task_name, shard_count):
193 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
194
195 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700196 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700197 task_name: name of the swarming task results belong to.
198 shard_count: expected number of task shards.
199 """
200 self.task_output_dir = task_output_dir
201 self.task_name = task_name
202 self.shard_count = shard_count
203
204 self._lock = threading.Lock()
205 self._per_shard_results = {}
206 self._storage = None
207
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700208 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700209 os.makedirs(self.task_output_dir)
210
Vadim Shtayurab450c602014-05-12 19:23:25 -0700211 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700212 """Stores results of a single task shard, fetches output files if necessary.
213
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400214 Modifies |result| in place.
215
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700216 Called concurrently from multiple threads.
217 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700218 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700219 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700220 if shard_index < 0 or shard_index >= self.shard_count:
221 logging.warning(
222 'Shard index %d is outside of expected range: [0; %d]',
223 shard_index, self.shard_count - 1)
224 return
225
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400226 assert not 'isolated_out' in result
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400227 result['isolated_out'] = None
228 for output in result['outputs']:
229 isolated_files_location = extract_output_files_location(output)
230 if isolated_files_location:
231 if result['isolated_out']:
232 raise ValueError('Unexpected two task with output')
233 result['isolated_out'] = isolated_files_location
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400234
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700235 # Store result dict of that shard, ignore results we've already seen.
236 with self._lock:
237 if shard_index in self._per_shard_results:
238 logging.warning('Ignoring duplicate shard index %d', shard_index)
239 return
240 self._per_shard_results[shard_index] = result
241
242 # Fetch output files if necessary.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400243 if self.task_output_dir and result['isolated_out']:
244 storage = self._get_storage(
245 result['isolated_out']['server'],
246 result['isolated_out']['namespace'])
247 if storage:
248 # Output files are supposed to be small and they are not reused across
249 # tasks. So use MemoryCache for them instead of on-disk cache. Make
250 # files writable, so that calling script can delete them.
251 isolateserver.fetch_isolated(
252 result['isolated_out']['hash'],
253 storage,
254 isolateserver.MemoryCache(file_mode_mask=0700),
255 os.path.join(self.task_output_dir, str(shard_index)),
256 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700257
258 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700259 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700260 with self._lock:
261 # Write an array of shard results with None for missing shards.
262 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700263 'shards': [
264 self._per_shard_results.get(i) for i in xrange(self.shard_count)
265 ],
266 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700267 # Write summary.json to task_output_dir as well.
268 if self.task_output_dir:
269 tools.write_json(
270 os.path.join(self.task_output_dir, 'summary.json'),
271 summary,
272 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700273 if self._storage:
274 self._storage.close()
275 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700276 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700277
278 def _get_storage(self, isolate_server, namespace):
279 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700280 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700281 with self._lock:
282 if not self._storage:
283 self._storage = isolateserver.get_storage(isolate_server, namespace)
284 else:
285 # Shards must all use exact same isolate server and namespace.
286 if self._storage.location != isolate_server:
287 logging.error(
288 'Task shards are using multiple isolate servers: %s and %s',
289 self._storage.location, isolate_server)
290 return None
291 if self._storage.namespace != namespace:
292 logging.error(
293 'Task shards are using multiple namespaces: %s and %s',
294 self._storage.namespace, namespace)
295 return None
296 return self._storage
297
298
maruel@chromium.org0437a732013-08-27 16:05:52 +0000299def now():
300 """Exists so it can be mocked easily."""
301 return time.time()
302
303
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700304def extract_output_files_location(task_log):
305 """Task log -> location of task output files to fetch.
306
307 TODO(vadimsh,maruel): Use side-channel to get this information.
308 See 'run_tha_test' in run_isolated.py for where the data is generated.
309
310 Returns:
311 Tuple (isolate server URL, namespace, isolated hash) on success.
312 None if information is missing or can not be parsed.
313 """
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400314 if not task_log:
315 return None
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700316 match = re.search(
317 r'\[run_isolated_out_hack\](.*)\[/run_isolated_out_hack\]',
318 task_log,
319 re.DOTALL)
320 if not match:
321 return None
322
323 def to_ascii(val):
324 if not isinstance(val, basestring):
325 raise ValueError()
326 return val.encode('ascii')
327
328 try:
329 data = json.loads(match.group(1))
330 if not isinstance(data, dict):
331 raise ValueError()
332 isolated_hash = to_ascii(data['hash'])
333 namespace = to_ascii(data['namespace'])
334 isolate_server = to_ascii(data['storage'])
335 if not file_path.is_url(isolate_server):
336 raise ValueError()
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400337 data = {
338 'hash': isolated_hash,
339 'namespace': namespace,
340 'server': isolate_server,
341 'view_url': '%s/browse?%s' % (isolate_server, urllib.urlencode(
342 [('namespace', namespace), ('hash', isolated_hash)])),
343 }
344 return data
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700345 except (KeyError, ValueError):
346 logging.warning(
347 'Unexpected value of run_isolated_out_hack: %s', match.group(1))
348 return None
349
350
351def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400352 base_url, shard_index, task_id, timeout, should_stop, output_collector):
353 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700354
Vadim Shtayurab450c602014-05-12 19:23:25 -0700355 Returns:
356 <result dict> on success.
357 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700358 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000359 assert isinstance(timeout, float), timeout
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400360 result_url = '%s/swarming/api/v1/client/task/%s' % (base_url, task_id)
361 output_url = '%s/swarming/api/v1/client/task/%s/output/all' % (
362 base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700363 started = now()
364 deadline = started + timeout if timeout else None
365 attempt = 0
366
367 while not should_stop.is_set():
368 attempt += 1
369
370 # Waiting for too long -> give up.
371 current_time = now()
372 if deadline and current_time >= deadline:
373 logging.error('retrieve_results(%s) timed out on attempt %d',
374 base_url, attempt)
375 return None
376
377 # Do not spin too fast. Spin faster at the beginning though.
378 # Start with 1 sec delay and for each 30 sec of waiting add another second
379 # of delay, until hitting 15 sec ceiling.
380 if attempt > 1:
381 max_delay = min(15, 1 + (current_time - started) / 30.0)
382 delay = min(max_delay, deadline - current_time) if deadline else max_delay
383 if delay > 0:
384 logging.debug('Waiting %.1f sec before retrying', delay)
385 should_stop.wait(delay)
386 if should_stop.is_set():
387 return None
388
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400389 # Disable internal retries in net.url_read_json, since we are doing retries
390 # ourselves.
391 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
392 result = net.url_read_json(result_url, retry_50x=False)
393 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400394 continue
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400395 if result['state'] in State.STATES_NOT_RUNNING:
396 out = net.url_read_json(output_url)
397 result['outputs'] = (out or {}).get('outputs', [])
398 if not result['outputs']:
399 logging.error('No output found for task %s', task_id)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400 # Record the result, try to fetch attached output files (if any).
401 if output_collector:
402 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700403 output_collector.process_shard_result(shard_index, result)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700404 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000405
406
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700407def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400408 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
409 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500410 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000411
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700412 Duplicate shards are ignored. Shards are yielded in order of completion.
413 Timed out shards are NOT yielded at all. Caller can compare number of yielded
414 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000415
416 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500417 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 +0000418 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500419
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700420 output_collector is an optional instance of TaskOutputCollector that will be
421 used to fetch files produced by a task from isolate server to the local disk.
422
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500423 Yields:
424 (index, result). In particular, 'result' is defined as the
425 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000426 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000427 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400428 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700429 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700430 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700431
maruel@chromium.org0437a732013-08-27 16:05:52 +0000432 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
433 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700434 # Adds a task to the thread pool to call 'retrieve_results' and return
435 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400436 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700437 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000438 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400439 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
440 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700441
442 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400443 for shard_index, task_id in enumerate(task_ids):
444 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700445
446 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400447 shards_remaining = range(len(task_ids))
448 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700449 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700450 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700451 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700452 shard_index, result = results_channel.pull(
453 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700454 except threading_utils.TaskChannel.Timeout:
455 if print_status_updates:
456 print(
457 'Waiting for results from the following shards: %s' %
458 ', '.join(map(str, shards_remaining)))
459 sys.stdout.flush()
460 continue
461 except Exception:
462 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700463
464 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700465 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000466 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500467 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000468 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700469
Vadim Shtayurab450c602014-05-12 19:23:25 -0700470 # Yield back results to the caller.
471 assert shard_index in shards_remaining
472 shards_remaining.remove(shard_index)
473 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700474
maruel@chromium.org0437a732013-08-27 16:05:52 +0000475 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700476 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000477 should_stop.set()
478
479
Vadim Shtayurab450c602014-05-12 19:23:25 -0700480def setup_run_isolated(manifest, bundle):
481 """Sets up the manifest to run an isolated task via run_isolated.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000482
Vadim Shtayurab450c602014-05-12 19:23:25 -0700483 Modifies |bundle| (by adding files) and |manifest| (by adding commands) in
484 place.
485
486 Args:
487 manifest: Manifest with swarm task definition.
488 bundle: ZipPackage with files that would be transfered to swarm bot.
489 If None, only |manifest| is modified (useful in tests).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000490 """
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000491 # Add uncompressed zip here. It'll be compressed as part of the package sent
492 # to Swarming server.
493 run_test_name = 'run_isolated.zip'
Vadim Shtayurab450c602014-05-12 19:23:25 -0700494 if bundle and run_test_name not in bundle.files:
495 bundle.add_buffer(
496 run_test_name,
497 run_isolated.get_as_zip_package().zip_into_buffer(compress=False))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000498
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000499 cleanup_script_name = 'swarm_cleanup.py'
Vadim Shtayurab450c602014-05-12 19:23:25 -0700500 if bundle and cleanup_script_name not in bundle.files:
501 bundle.add_file(
502 os.path.join(TOOLS_PATH, cleanup_script_name), cleanup_script_name)
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000503
maruel@chromium.org0437a732013-08-27 16:05:52 +0000504 run_cmd = [
505 'python', run_test_name,
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000506 '--hash', manifest.isolated_hash,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500507 '--namespace', manifest.namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000508 ]
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500509 if file_path.is_url(manifest.isolate_server):
510 run_cmd.extend(('--isolate-server', manifest.isolate_server))
511 else:
512 run_cmd.extend(('--indir', manifest.isolate_server))
513
maruel@chromium.org0437a732013-08-27 16:05:52 +0000514 if manifest.verbose or manifest.profile:
515 # Have it print the profiling section.
516 run_cmd.append('--verbose')
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700517
518 # Pass all extra args for run_isolated.py, it will pass them to the command.
519 if manifest.extra_args:
520 run_cmd.append('--')
521 run_cmd.extend(manifest.extra_args)
522
maruel@chromium.org0437a732013-08-27 16:05:52 +0000523 manifest.add_task('Run Test', run_cmd)
524
525 # Clean up
526 manifest.add_task('Clean Up', ['python', cleanup_script_name])
527
528
Vadim Shtayurab450c602014-05-12 19:23:25 -0700529def setup_googletest(env, shards, index):
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500530 """Sets googletest specific environment variables."""
531 if shards > 1:
532 env = env.copy()
Vadim Shtayurab450c602014-05-12 19:23:25 -0700533 env['GTEST_SHARD_INDEX'] = str(index)
534 env['GTEST_TOTAL_SHARDS'] = str(shards)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500535 return env
536
537
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500538def archive(isolate_server, namespace, isolated, algo, verbose):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000539 """Archives a .isolated and all the dependencies on the CAC."""
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500540 logging.info('archive(%s, %s, %s)', isolate_server, namespace, isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000541 tempdir = None
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500542 if file_path.is_url(isolate_server):
543 command = 'archive'
544 flag = '--isolate-server'
545 else:
546 command = 'hashtable'
547 flag = '--outdir'
548
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500549 print('Archiving: %s' % isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000550 try:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000551 cmd = [
552 sys.executable,
553 os.path.join(ROOT_DIR, 'isolate.py'),
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500554 command,
555 flag, isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500556 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000557 '--isolated', isolated,
558 ]
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000559 cmd.extend(['--verbose'] * verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000560 logging.info(' '.join(cmd))
561 if subprocess.call(cmd, verbose):
562 return
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400563 return isolated_format.hash_file(isolated, algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000564 finally:
565 if tempdir:
566 shutil.rmtree(tempdir)
567
568
Vadim Shtayurab450c602014-05-12 19:23:25 -0700569def get_shard_task_name(task_name, shards, index):
570 """Returns a task name to use for a single shard of a task."""
571 if shards == 1:
572 return task_name
573 return '%s:%s:%s' % (task_name, shards, index)
574
575
576def upload_zip_bundle(isolate_server, bundle):
577 """Uploads a zip package to isolate storage and returns raw fetch URL.
578
579 Args:
580 isolate_server: URL of an isolate server.
581 bundle: instance of ZipPackage to upload.
582
583 Returns:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400584 URL to get the file from.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700585 """
586 # Swarming bot would need to be able to grab the file from the storage
587 # using raw HTTP GET. Use 'default' namespace so that the raw data returned
588 # to a bot is not zipped, since swarm_bot doesn't understand compressed
589 # data yet. This namespace have nothing to do with |namespace| passed to
590 # run_isolated.py that is used to store files for isolated task.
591 logging.info('Zipping up and uploading files...')
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400592 start_time = now()
593 isolate_item = isolateserver.BufferItem(
594 bundle.zip_into_buffer(), high_priority=True)
595 with isolateserver.get_storage(isolate_server, 'default') as storage:
596 uploaded = storage.upload_items([isolate_item])
597 bundle_url = storage.get_fetch_url(isolate_item)
598 elapsed = now() - start_time
Vadim Shtayurab450c602014-05-12 19:23:25 -0700599 if isolate_item in uploaded:
600 logging.info('Upload complete, time elapsed: %f', elapsed)
601 else:
602 logging.info('Zip file already on server, time elapsed: %f', elapsed)
603 return bundle_url
604
605
606def trigger_by_manifest(swarming, manifest):
607 """Given a task manifest, triggers it for execution on swarming.
608
609 Args:
610 swarming: URL of a swarming service.
611 manifest: instance of Manifest.
612
613 Returns:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400614 tuple(Task id, priority) on success. tuple(None, None) on failure.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700615 """
616 logging.info('Triggering: %s', manifest.task_name)
617 manifest_text = manifest.to_json()
618 result = net.url_read(swarming + '/test', data={'request': manifest_text})
619 if not result:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400620 on_error.report('Failed to trigger task %s' % manifest.task_name)
Vadim Shtayura1c024f72014-07-09 19:00:10 -0700621 return None, None
Vadim Shtayurab450c602014-05-12 19:23:25 -0700622 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400623 data = json.loads(result)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400624 except (ValueError, TypeError):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700625 msg = '\n'.join((
626 'Failed to trigger task %s' % manifest.task_name,
627 'Manifest: %s' % manifest_text,
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400628 'Bad response: %s' % result))
629 on_error.report(msg)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400630 return None, None
631 if not data:
632 return None, None
633 return data['test_keys'][0]['test_key'], data['priority']
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634
635
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400636def abort_task(_swarming, _manifest):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700637 """Given a task manifest that was triggered, aborts its execution."""
638 # TODO(vadimsh): No supported by the server yet.
639
640
641def trigger_task_shards(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700642 swarming, isolate_server, namespace, isolated_hash, task_name, extra_args,
Marc-Antoine Ruelaea50652014-06-12 14:23:48 -0400643 shards, dimensions, env, deadline, verbose, profile, priority):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400644 """Triggers multiple subtasks of a sharded task.
645
646 Returns:
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700647 Dict with task details, returned to caller as part of --dump-json output.
648 None in case of failure.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400649 """
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 # Collects all files that are necessary to bootstrap a task execution
651 # on the bot. Usually it includes self contained run_isolated.zip and
652 # a bunch of small other scripts. All heavy files are pulled
653 # by run_isolated.zip. Updated in 'setup_run_isolated'.
654 bundle = zip_package.ZipPackage(ROOT_DIR)
655
656 # Make a separate Manifest for each shard, put shard index and number of
657 # shards into env and subtask name.
658 manifests = []
659 for index in xrange(shards):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000660 manifest = Manifest(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500661 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500662 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500663 isolated_hash=isolated_hash,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700664 task_name=get_shard_task_name(task_name, shards, index),
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700665 extra_args=extra_args,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500666 dimensions=dimensions,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700667 env=setup_googletest(env, shards, index),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400668 deadline=deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500669 verbose=verbose,
670 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800671 priority=priority)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700672 setup_run_isolated(manifest, bundle)
673 manifests.append(manifest)
674
675 # Upload zip bundle file to get its URL.
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400676 try:
677 bundle_url = upload_zip_bundle(isolate_server, bundle)
678 except (IOError, OSError):
679 on_error.report('Failed to upload the zip file for task %s' % task_name)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400680 return None, None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681
Vadim Shtayurab450c602014-05-12 19:23:25 -0700682 # Attach that file to all manifests.
683 for manifest in manifests:
684 manifest.add_bundled_file('swarm_data.zip', bundle_url)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000685
Vadim Shtayurab450c602014-05-12 19:23:25 -0700686 # Trigger all the subtasks.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400687 tasks = {}
688 priority_warning = False
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700689 for index, manifest in enumerate(manifests):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400690 task_id, priority = trigger_by_manifest(swarming, manifest)
691 if not task_id:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700692 break
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400693 if not priority_warning and priority != manifest.priority:
694 priority_warning = True
695 print >> sys.stderr, 'Priority was reset to %s' % priority
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700696 tasks[manifest.task_name] = {
697 'shard_index': index,
698 'task_id': task_id,
699 'view_url': '%s/user/task/%s' % (swarming, task_id),
700 }
Vadim Shtayurab450c602014-05-12 19:23:25 -0700701
702 # Some shards weren't triggered. Abort everything.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400703 if len(tasks) != len(manifests):
704 if tasks:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700705 print >> sys.stderr, 'Not all shards were triggered'
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700706 for task_dict in tasks.itervalues():
707 abort_task(swarming, task_dict['task_id'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400708 return None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000709
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400710 return tasks
maruel@chromium.org0437a732013-08-27 16:05:52 +0000711
712
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500713def isolated_to_hash(isolate_server, namespace, arg, algo, verbose):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500714 """Archives a .isolated file if needed.
715
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500716 Returns the file hash to trigger and a bool specifying if it was a file (True)
717 or a hash (False).
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500718 """
719 if arg.endswith('.isolated'):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500720 file_hash = archive(isolate_server, namespace, arg, algo, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500721 if not file_hash:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400722 on_error.report('Archival failure %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500723 return None, True
724 return file_hash, True
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400725 elif isolated_format.is_valid_hash(arg, algo):
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500726 return arg, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500727 else:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400728 on_error.report('Invalid hash %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500729 return None, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500730
731
maruel@chromium.org0437a732013-08-27 16:05:52 +0000732def trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500733 swarming,
734 isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500735 namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500736 file_hash_or_isolated,
737 task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700738 extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500739 shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500740 dimensions,
741 env,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400742 deadline,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000743 verbose,
744 profile,
745 priority):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400746 """Sends off the hash swarming task requests.
747
748 Returns:
749 tuple(dict(task_name: task_id), base task name). The dict of tasks is None
750 in case of failure.
751 """
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500752 file_hash, is_file = isolated_to_hash(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500753 isolate_server, namespace, file_hash_or_isolated, hashlib.sha1, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500754 if not file_hash:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500755 return 1, ''
756 if not task_name:
757 # If a file name was passed, use its base name of the isolated hash.
758 # Otherwise, use user name as an approximation of a task name.
759 if is_file:
760 key = os.path.splitext(os.path.basename(file_hash_or_isolated))[0]
761 else:
762 key = getpass.getuser()
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700763 task_name = '%s/%s/%s/%d' % (
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500764 key,
765 '_'.join('%s=%s' % (k, v) for k, v in sorted(dimensions.iteritems())),
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700766 file_hash,
767 now() * 1000)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500768
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400769 tasks = trigger_task_shards(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500770 swarming=swarming,
771 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500772 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500773 isolated_hash=file_hash,
774 task_name=task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700775 extra_args=extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500776 shards=shards,
777 dimensions=dimensions,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400778 deadline=deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500779 env=env,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500780 verbose=verbose,
781 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800782 priority=priority)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400783 return tasks, task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +0000784
785
Vadim Shtayurab450c602014-05-12 19:23:25 -0700786def decorate_shard_output(shard_index, result, shard_exit_code):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000787 """Returns wrapped output for swarming task shard."""
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400788 tag_shard = '%d (Bot: %s)' % (shard_index, result['bot_id'])
789 tag_header = 'Shard %s' % tag_shard
790 tag_footer = 'End of shard %s; exit code %d' % (tag_shard, shard_exit_code)
791
792 tag_len = max(len(tag_header), len(tag_footer))
793 dash_pad = '+-%s-+\n' % ('-' * tag_len)
794 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
795 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
796
797 header = dash_pad + tag_header + dash_pad
798 footer = dash_pad + tag_footer + dash_pad[:-1]
799 output = '\n'.join(o for o in result['outputs'] if o).rstrip() + '\n'
800 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000801
802
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700803def collect(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400804 url, task_name, task_ids, timeout, decorate, print_status_updates,
805 task_summary_json, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500806 """Retrieves results of a Swarming task."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700807 # Collect summary JSON and output files (if task_output_dir is not None).
808 output_collector = TaskOutputCollector(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400809 task_output_dir, task_name, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700810
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700811 seen_shards = set()
Vadim Shtayurac524f512014-05-15 09:54:56 -0700812 exit_codes = []
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700813 try:
814 for index, output in yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400815 url, task_ids, timeout, None, print_status_updates, output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700816 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700817
818 # Grab first non-zero exit code as an overall shard exit code.
819 shard_exit_code = 0
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400820 for code in output['exit_codes']:
Vadim Shtayura473455a2014-05-14 15:22:35 -0700821 if code:
822 shard_exit_code = code
823 break
Vadim Shtayurac524f512014-05-15 09:54:56 -0700824 exit_codes.append(shard_exit_code)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700825
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700826 if decorate:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400827 print(decorate_shard_output(index, output, shard_exit_code))
828 if len(seen_shards) < len(task_ids):
829 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700830 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400831 print('%s: %s' %
832 (output['bot_id'], ','.join(str(e) for e in output['exit_codes'])))
833 # TODO(maruel): Print the command.
834 for output in output['outputs']:
835 if not output:
836 continue
837 output = output.rstrip()
838 if output:
839 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700840 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700841 summary = output_collector.finalize()
842 if task_summary_json:
843 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700844
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400845 if len(seen_shards) != len(task_ids):
846 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700847 print >> sys.stderr, ('Results from some shards are missing: %s' %
848 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700849 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700850
Vadim Shtayurac524f512014-05-15 09:54:56 -0700851 return int(bool(any(exit_codes)))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000852
853
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400854def add_filter_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500855 parser.filter_group = tools.optparse.OptionGroup(parser, 'Filtering slaves')
856 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500857 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500858 dest='dimensions', metavar='FOO bar',
859 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500860 parser.add_option_group(parser.filter_group)
861
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400862
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400863def process_filter_options(parser, options):
864 options.dimensions = dict(options.dimensions)
865 if not options.dimensions:
866 parser.error('Please at least specify one --dimension')
867
868
Vadim Shtayurab450c602014-05-12 19:23:25 -0700869def add_sharding_options(parser):
870 parser.sharding_group = tools.optparse.OptionGroup(parser, 'Sharding options')
871 parser.sharding_group.add_option(
872 '--shards', type='int', default=1,
873 help='Number of shards to trigger and collect.')
874 parser.add_option_group(parser.sharding_group)
875
876
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400877def add_trigger_options(parser):
878 """Adds all options to trigger a task on Swarming."""
879 isolateserver.add_isolate_server_options(parser, True)
880 add_filter_options(parser)
881
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500882 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
883 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500884 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700885 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500886 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500887 '--priority', type='int', default=100,
888 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500889 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500890 '-T', '--task-name',
891 help='Display name of the task. It uniquely identifies the task. '
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700892 'Defaults to <base_name>/<dimensions>/<isolated hash>/<timestamp> '
893 'if an isolated file is provided, if a hash is provided, it '
894 'defaults to <user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400895 parser.task_group.add_option(
896 '--deadline', type='int', default=6*60*60,
897 help='Seconds to allow the task to be pending for a bot to run before '
898 'this task request expires.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500899 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500900 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500901 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000902 '--profile', action='store_true',
903 default=bool(os.environ.get('ISOLATE_DEBUG')),
904 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000905
906
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500907def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500908 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500909 if len(args) != 1:
910 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400911 process_filter_options(parser, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000912
913
914def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500915 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000916 '-t', '--timeout',
917 type='float',
918 default=DEFAULT_SHARD_WAIT_TIME,
919 help='Timeout to wait for result, set to 0 for no timeout; default: '
920 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500921 parser.group_logging.add_option(
922 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700923 parser.group_logging.add_option(
924 '--print-status-updates', action='store_true',
925 help='Print periodic status updates')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700926 parser.task_output_group = tools.optparse.OptionGroup(parser, 'Task output')
927 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700928 '--task-summary-json',
929 metavar='FILE',
930 help='Dump a summary of task results to this file as json. It contains '
931 'only shards statuses as know to server directly. Any output files '
932 'emitted by the task can be collected by using --task-output-dir')
933 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700934 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700935 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700936 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700937 'directory contains per-shard directory with output files produced '
938 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700939 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000940
941
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700942def extract_isolated_command_extra_args(args):
943 try:
944 index = args.index('--')
945 except ValueError:
946 return (args, [])
947 return (args[:index], args[index+1:])
948
949
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400950def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400951 """Returns information about the bots connected to the Swarming server."""
952 add_filter_options(parser)
953 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400954 '--dead-only', action='store_true',
955 help='Only print dead bots, useful to reap them and reimage broken bots')
956 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400957 '-k', '--keep-dead', action='store_true',
958 help='Do not filter out dead bots')
959 parser.filter_group.add_option(
960 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400961 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400962 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400963
964 if options.keep_dead and options.dead_only:
965 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700966
967 auth.ensure_logged_in(options.swarming)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400968
969 bots = []
970 cursor = None
971 limit = 250
972 # Iterate via cursors.
973 base_url = options.swarming + '/swarming/api/v1/client/bots?limit=%d' % limit
974 while True:
975 url = base_url
976 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400977 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400978 data = net.url_read_json(url)
979 if data is None:
980 print >> sys.stderr, 'Failed to access %s' % options.swarming
981 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400982 bots.extend(data['items'])
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400983 cursor = data['cursor']
984 if not cursor:
985 break
986
987 for bot in natsort.natsorted(bots, key=lambda x: x['id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400988 if options.dead_only:
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400989 if not bot['is_dead']:
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400990 continue
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400991 elif not options.keep_dead and bot['is_dead']:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400992 continue
993
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400994 # If the user requested to filter on dimensions, ensure the bot has all the
995 # dimensions requested.
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400996 dimensions = bot['dimensions']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400997 for key, value in options.dimensions:
998 if key not in dimensions:
999 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001000 # A bot can have multiple value for a key, for example,
1001 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1002 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001003 if isinstance(dimensions[key], list):
1004 if value not in dimensions[key]:
1005 break
1006 else:
1007 if value != dimensions[key]:
1008 break
1009 else:
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001010 print bot['id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001011 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001012 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001013 if bot['task']:
1014 print ' task: %s' % bot['task']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001015 return 0
1016
1017
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001018@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001019def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001020 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001021
1022 The result can be in multiple part if the execution was sharded. It can
1023 potentially have retries.
1024 """
1025 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001026 parser.add_option(
1027 '-j', '--json',
1028 help='Load the task ids from .json as saved by trigger --dump-json')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001029 (options, args) = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001030 if not args and not options.json:
1031 parser.error('Must specify at least one task id or --json.')
1032 if args and options.json:
1033 parser.error('Only use one of task id or --json.')
1034
1035 if options.json:
1036 with open(options.json) as f:
1037 tasks = sorted(
1038 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1039 args = [t['task_id'] for t in tasks]
1040 else:
1041 valid = frozenset('0123456789abcdef')
1042 if any(not valid.issuperset(task_id) for task_id in args):
1043 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001044
1045 auth.ensure_logged_in(options.swarming)
1046 try:
1047 return collect(
1048 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001049 None,
1050 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001051 options.timeout,
1052 options.decorate,
1053 options.print_status_updates,
1054 options.task_summary_json,
1055 options.task_output_dir)
1056 except Failure:
1057 on_error.report(None)
1058 return 1
1059
1060
1061@subcommand.usage('[resource name]')
1062def CMDquery(parser, args):
1063 """Returns raw JSON information via an URL endpoint. Use 'list' to gather the
1064 list of valid values from the server.
1065
1066 Examples:
1067 Printing the list of known URLs:
1068 swarming.py query -S https://server-url list
1069
1070 Listing last 50 tasks on a specific bot named 'swarm1'
1071 swarming.py query -S https://server-url --limit 50 bot/swarm1/tasks
1072 """
1073 CHUNK_SIZE = 250
1074
1075 parser.add_option(
1076 '-L', '--limit', type='int', default=200,
1077 help='Limit to enforce on limitless items (like number of tasks); '
1078 'default=%default')
1079 (options, args) = parser.parse_args(args)
1080 if len(args) != 1:
1081 parser.error('Must specify only one resource name.')
1082
1083 auth.ensure_logged_in(options.swarming)
1084
1085 base_url = options.swarming + '/swarming/api/v1/client/' + args[0]
1086 url = base_url
1087 if options.limit:
1088 url += '?limit=%d' % min(CHUNK_SIZE, options.limit)
1089 data = net.url_read_json(url)
1090 if data is None:
1091 print >> sys.stderr, 'Failed to access %s' % options.swarming
1092 return 1
1093
1094 # Some items support cursors. Try to get automatically if cursors are needed
1095 # by looking at the 'cursor' items.
1096 while (
1097 data.get('cursor') and
1098 (not options.limit or len(data['items']) < options.limit)):
1099 url = base_url + '?cursor=%s' % urllib.quote(data['cursor'])
1100 if options.limit:
1101 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
1102 new = net.url_read_json(url)
1103 if new is None:
1104 print >> sys.stderr, 'Failed to access %s' % options.swarming
1105 return 1
1106 data['items'].extend(new['items'])
1107 data['cursor'] = new['cursor']
1108
1109 if options.limit and len(data.get('items', [])) > options.limit:
1110 data['items'] = data['items'][:options.limit]
1111 data.pop('cursor', None)
1112
1113 json.dump(data, sys.stdout, indent=2, sort_keys=True)
1114 sys.stdout.write('\n')
1115 return 0
1116
1117
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001118@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001119def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001120 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001121
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001122 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001123 """
1124 add_trigger_options(parser)
1125 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001126 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001127 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001128 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001129 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001130
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001131 auth.ensure_logged_in(options.swarming)
1132 if file_path.is_url(options.isolate_server):
1133 auth.ensure_logged_in(options.isolate_server)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001134 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001135 tasks, task_name = trigger(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001136 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001137 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001138 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001139 file_hash_or_isolated=args[0],
1140 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001141 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001142 shards=options.shards,
1143 dimensions=options.dimensions,
1144 env=dict(options.env),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -04001145 deadline=options.deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001146 verbose=options.verbose,
1147 profile=options.profile,
1148 priority=options.priority)
1149 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001150 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001151 'Failed to trigger %s(%s): %s' %
1152 (options.task_name, args[0], e.args[0]))
1153 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001154 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001155 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001156 return 1
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001157 if task_name != options.task_name:
1158 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001159 task_ids = [
1160 t['task_id']
1161 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1162 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001163 try:
1164 return collect(
1165 options.swarming,
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001166 task_name,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001167 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001168 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001169 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001170 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001171 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001172 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001173 except Failure:
1174 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001175 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001176
1177
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001178@subcommand.usage("(hash|isolated) [-- extra_args]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001179def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001180 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001181
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001182 Accepts either the hash (sha1) of a .isolated file already uploaded or the
1183 path to an .isolated file to archive, packages it if needed and sends a
1184 Swarming manifest file to the Swarming server.
1185
1186 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001187
1188 Passes all extra arguments provided after '--' as additional command line
1189 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001190 """
1191 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001192 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001193 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001194 parser.add_option(
1195 '--dump-json',
1196 metavar='FILE',
1197 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001198 options, args = parser.parse_args(args)
1199 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001200
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001201 auth.ensure_logged_in(options.swarming)
1202 if file_path.is_url(options.isolate_server):
1203 auth.ensure_logged_in(options.isolate_server)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001204 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001205 tasks, task_name = trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001206 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001207 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001208 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001209 file_hash_or_isolated=args[0],
1210 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001211 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001212 shards=options.shards,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001213 dimensions=options.dimensions,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -05001214 env=dict(options.env),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -04001215 deadline=options.deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001216 verbose=options.verbose,
1217 profile=options.profile,
1218 priority=options.priority)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001219 if tasks:
1220 if task_name != options.task_name:
1221 print('Triggered task: %s' % task_name)
1222 if options.dump_json:
1223 data = {
1224 'base_task_name': task_name,
1225 'tasks': tasks,
1226 }
1227 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001228 print('To collect results, use:')
1229 print(' swarming.py collect -S %s --json %s' %
1230 (options.swarming, options.dump_json))
1231 else:
1232 task_ids = [
1233 t['task_id']
1234 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1235 ]
1236 print('To collect results, use:')
1237 print(' swarming.py collect -S %s %s' %
1238 (options.swarming, ' '.join(task_ids)))
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001239 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001240 except Failure:
1241 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001242 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001243
1244
1245class OptionParserSwarming(tools.OptionParserWithLogging):
1246 def __init__(self, **kwargs):
1247 tools.OptionParserWithLogging.__init__(
1248 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001249 self.server_group = tools.optparse.OptionGroup(self, 'Server')
1250 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001251 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001252 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001253 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001254 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001255 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001256
1257 def parse_args(self, *args, **kwargs):
1258 options, args = tools.OptionParserWithLogging.parse_args(
1259 self, *args, **kwargs)
1260 options.swarming = options.swarming.rstrip('/')
1261 if not options.swarming:
1262 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001263 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001264 return options, args
1265
1266
1267def main(args):
1268 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001269 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001270
1271
1272if __name__ == '__main__':
1273 fix_encoding.fix_encoding()
1274 tools.disable_buffering()
1275 colorama.init()
1276 sys.exit(main(sys.argv[1:]))