blob: 3afb688a23a758398689197bf7f12f34172b21d5 [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 Ruel79940ae2014-09-23 17:55:41 -04008__version__ = '0.4.16'
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
maruel@chromium.org0437a732013-08-27 16:05:52 +000051
52NO_OUTPUT_FOUND = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050053 'No output produced by the task, it may have failed to run.\n'
maruel@chromium.org0437a732013-08-27 16:05:52 +000054 '\n')
55
56
maruel@chromium.org0437a732013-08-27 16:05:52 +000057class Failure(Exception):
58 """Generic failure."""
59 pass
60
61
62class Manifest(object):
Vadim Shtayurab450c602014-05-12 19:23:25 -070063 """Represents a Swarming task manifest."""
maruel@chromium.org0437a732013-08-27 16:05:52 +000064
maruel@chromium.org0437a732013-08-27 16:05:52 +000065 def __init__(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070066 self, isolate_server, namespace, isolated_hash, task_name, extra_args,
Marc-Antoine Ruelaea50652014-06-12 14:23:48 -040067 env, dimensions, deadline, verbose, profile,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070068 priority):
maruel@chromium.org0437a732013-08-27 16:05:52 +000069 """Populates a manifest object.
70 Args:
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050071 isolate_server - isolate server url.
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050072 namespace - isolate server namespace to use.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070073 isolated_hash - the manifest's sha-1 that the slave is going to fetch.
74 task_name - the name to give the task request.
75 extra_args - additional arguments to pass to isolated command.
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -050076 env - environment variables to set.
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050077 dimensions - dimensions to filter the task on.
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -040078 deadline - maximum pending time before this task expires.
maruel@chromium.org0437a732013-08-27 16:05:52 +000079 verbose - if True, have the slave print more details.
80 profile - if True, have the slave print more timing data.
maruel@chromium.org7b844a62013-09-17 13:04:59 +000081 priority - int between 0 and 1000, lower the higher priority.
maruel@chromium.org0437a732013-08-27 16:05:52 +000082 """
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050083 self.isolate_server = isolate_server
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050084 self.namespace = namespace
maruel@chromium.org814d23f2013-10-01 19:08:00 +000085 self.isolated_hash = isolated_hash
Vadim Shtayurab450c602014-05-12 19:23:25 -070086 self.task_name = task_name
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070087 self.extra_args = tuple(extra_args or [])
Vadim Shtayurab450c602014-05-12 19:23:25 -070088 self.env = env.copy()
89 self.dimensions = dimensions.copy()
Vadim Shtayurab450c602014-05-12 19:23:25 -070090 self.deadline = deadline
maruel@chromium.org0437a732013-08-27 16:05:52 +000091 self.verbose = bool(verbose)
92 self.profile = bool(profile)
93 self.priority = priority
maruel@chromium.org0437a732013-08-27 16:05:52 +000094 self._tasks = []
Vadim Shtayurab450c602014-05-12 19:23:25 -070095 self._files = []
maruel@chromium.org0437a732013-08-27 16:05:52 +000096
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -040097 def add_task(self, task_name, actions, time_out=2*60*60):
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -050098 """Appends a new task as a TestObject to the swarming manifest file.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -050099
100 Tasks cannot be added once the manifest was uploaded.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500101
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400102 By default, command will be killed after 2 hours of execution.
103
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500104 See TestObject in services/swarming/src/common/test_request_message.py for
105 the valid format.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500106 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000107 self._tasks.append(
108 {
109 'action': actions,
110 'decorate_output': self.verbose,
111 'test_name': task_name,
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400112 'hard_time_out': time_out,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000113 })
114
Vadim Shtayurab450c602014-05-12 19:23:25 -0700115 def add_bundled_file(self, file_name, file_url):
116 """Appends a file to the manifest.
117
118 File will be downloaded and extracted by the swarm bot before launching the
119 task.
120 """
121 self._files.append([file_url, file_name])
122
maruel@chromium.org0437a732013-08-27 16:05:52 +0000123 def to_json(self):
124 """Exports the current configuration into a swarm-readable manifest file.
125
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500126 The actual serialization format is defined as a TestCase object as described
127 in services/swarming/src/common/test_request_message.py
maruel@chromium.org0437a732013-08-27 16:05:52 +0000128 """
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500129 request = {
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500130 'cleanup': 'root',
maruel@chromium.org0437a732013-08-27 16:05:52 +0000131 'configurations': [
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500132 # Is a TestConfiguration.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000133 {
Marc-Antoine Ruel5d799192013-11-06 15:20:39 -0500134 'config_name': 'isolated',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700135 'deadline_to_run': self.deadline,
136 'dimensions': self.dimensions,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500137 'priority': self.priority,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000138 },
139 ],
Vadim Shtayurab450c602014-05-12 19:23:25 -0700140 'data': self._files,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700141 'env_vars': self.env,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700142 'test_case_name': self.task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500143 'tests': self._tasks,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000144 }
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500145 return json.dumps(request, sort_keys=True, separators=(',',':'))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000146
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500147
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700148class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700149 """Assembles task execution summary (for --task-summary-json output).
150
151 Optionally fetches task outputs from isolate server to local disk (used when
152 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700153
154 This object is shared among multiple threads running 'retrieve_results'
155 function, in particular they call 'process_shard_result' method in parallel.
156 """
157
158 def __init__(self, task_output_dir, task_name, shard_count):
159 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
160
161 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700162 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700163 task_name: name of the swarming task results belong to.
164 shard_count: expected number of task shards.
165 """
166 self.task_output_dir = task_output_dir
167 self.task_name = task_name
168 self.shard_count = shard_count
169
170 self._lock = threading.Lock()
171 self._per_shard_results = {}
172 self._storage = None
173
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700174 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700175 os.makedirs(self.task_output_dir)
176
Vadim Shtayurab450c602014-05-12 19:23:25 -0700177 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700178 """Stores results of a single task shard, fetches output files if necessary.
179
180 Called concurrently from multiple threads.
181 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700182 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700183 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700184 if shard_index < 0 or shard_index >= self.shard_count:
185 logging.warning(
186 'Shard index %d is outside of expected range: [0; %d]',
187 shard_index, self.shard_count - 1)
188 return
189
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400190 result = result.copy()
191
192 isolated_files_location = extract_output_files_location(result['output'])
193 if isolated_files_location:
194 result['isolated_out'] = isolated_files_location
195
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700196 # Store result dict of that shard, ignore results we've already seen.
197 with self._lock:
198 if shard_index in self._per_shard_results:
199 logging.warning('Ignoring duplicate shard index %d', shard_index)
200 return
201 self._per_shard_results[shard_index] = result
202
203 # Fetch output files if necessary.
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700204 if self.task_output_dir:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700205 if isolated_files_location:
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400206 storage = self._get_storage(
207 isolated_files_location['server'],
208 isolated_files_location['namespace'])
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700209 if storage:
210 # Output files are supposed to be small and they are not reused across
211 # tasks. So use MemoryCache for them instead of on-disk cache. Make
212 # files writable, so that calling script can delete them.
213 isolateserver.fetch_isolated(
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400214 isolated_files_location['hash'],
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700215 storage,
216 isolateserver.MemoryCache(file_mode_mask=0700),
217 os.path.join(self.task_output_dir, str(shard_index)),
218 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700219
220 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700221 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700222 with self._lock:
223 # Write an array of shard results with None for missing shards.
224 summary = {
225 'task_name': self.task_name,
226 'shards': [
227 self._per_shard_results.get(i) for i in xrange(self.shard_count)
228 ],
229 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700230 # Write summary.json to task_output_dir as well.
231 if self.task_output_dir:
232 tools.write_json(
233 os.path.join(self.task_output_dir, 'summary.json'),
234 summary,
235 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700236 if self._storage:
237 self._storage.close()
238 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700239 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700240
241 def _get_storage(self, isolate_server, namespace):
242 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700243 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700244 with self._lock:
245 if not self._storage:
246 self._storage = isolateserver.get_storage(isolate_server, namespace)
247 else:
248 # Shards must all use exact same isolate server and namespace.
249 if self._storage.location != isolate_server:
250 logging.error(
251 'Task shards are using multiple isolate servers: %s and %s',
252 self._storage.location, isolate_server)
253 return None
254 if self._storage.namespace != namespace:
255 logging.error(
256 'Task shards are using multiple namespaces: %s and %s',
257 self._storage.namespace, namespace)
258 return None
259 return self._storage
260
261
maruel@chromium.org0437a732013-08-27 16:05:52 +0000262def now():
263 """Exists so it can be mocked easily."""
264 return time.time()
265
266
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500267def get_task_keys(swarm_base_url, task_name):
268 """Returns the Swarming task key for each shards of task_name."""
269 key_data = urllib.urlencode([('name', task_name)])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000270 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data)
271
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000272 for _ in net.retry_loop(max_attempts=net.URL_OPEN_MAX_ATTEMPTS):
273 result = net.url_read(url, retry_404=True)
274 if result is None:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000275 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500276 'Error: Unable to find any task with the name, %s, on swarming server'
277 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000278
maruel@chromium.org0437a732013-08-27 16:05:52 +0000279 # TODO(maruel): Compare exact string.
280 if 'No matching' in result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500281 logging.warning('Unable to find any task with the name, %s, on swarming '
282 'server' % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000283 continue
284 return json.loads(result)
285
286 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500287 'Error: Unable to find any task with the name, %s, on swarming server'
288 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000289
290
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700291def extract_output_files_location(task_log):
292 """Task log -> location of task output files to fetch.
293
294 TODO(vadimsh,maruel): Use side-channel to get this information.
295 See 'run_tha_test' in run_isolated.py for where the data is generated.
296
297 Returns:
298 Tuple (isolate server URL, namespace, isolated hash) on success.
299 None if information is missing or can not be parsed.
300 """
301 match = re.search(
302 r'\[run_isolated_out_hack\](.*)\[/run_isolated_out_hack\]',
303 task_log,
304 re.DOTALL)
305 if not match:
306 return None
307
308 def to_ascii(val):
309 if not isinstance(val, basestring):
310 raise ValueError()
311 return val.encode('ascii')
312
313 try:
314 data = json.loads(match.group(1))
315 if not isinstance(data, dict):
316 raise ValueError()
317 isolated_hash = to_ascii(data['hash'])
318 namespace = to_ascii(data['namespace'])
319 isolate_server = to_ascii(data['storage'])
320 if not file_path.is_url(isolate_server):
321 raise ValueError()
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400322 data = {
323 'hash': isolated_hash,
324 'namespace': namespace,
325 'server': isolate_server,
326 'view_url': '%s/browse?%s' % (isolate_server, urllib.urlencode(
327 [('namespace', namespace), ('hash', isolated_hash)])),
328 }
329 return data
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700330 except (KeyError, ValueError):
331 logging.warning(
332 'Unexpected value of run_isolated_out_hack: %s', match.group(1))
333 return None
334
335
336def retrieve_results(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700337 base_url, shard_index, task_key, timeout, should_stop, output_collector):
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700338 """Retrieves results for a single task_key.
339
Vadim Shtayurab450c602014-05-12 19:23:25 -0700340 Returns:
341 <result dict> on success.
342 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700343 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000344 assert isinstance(timeout, float), timeout
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500345 params = [('r', task_key)]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000346 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700347 started = now()
348 deadline = started + timeout if timeout else None
349 attempt = 0
350
351 while not should_stop.is_set():
352 attempt += 1
353
354 # Waiting for too long -> give up.
355 current_time = now()
356 if deadline and current_time >= deadline:
357 logging.error('retrieve_results(%s) timed out on attempt %d',
358 base_url, attempt)
359 return None
360
361 # Do not spin too fast. Spin faster at the beginning though.
362 # Start with 1 sec delay and for each 30 sec of waiting add another second
363 # of delay, until hitting 15 sec ceiling.
364 if attempt > 1:
365 max_delay = min(15, 1 + (current_time - started) / 30.0)
366 delay = min(max_delay, deadline - current_time) if deadline else max_delay
367 if delay > 0:
368 logging.debug('Waiting %.1f sec before retrying', delay)
369 should_stop.wait(delay)
370 if should_stop.is_set():
371 return None
372
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400373 # Disable internal retries in net.url_read, since we are doing retries
374 # ourselves. Do not use retry_404 so should_stop is polled more often.
375 response = net.url_read(result_url, retry_404=False, retry_50x=False)
376
377 # Request failed. Try again.
378 if response is None:
379 continue
380
381 # Got some response, ensure it is JSON dict, retry if not.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700382 try:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400383 result = json.loads(response) or {}
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700384 if not isinstance(result, dict):
385 raise ValueError()
386 except (ValueError, TypeError):
387 logging.warning(
388 'Received corrupted or invalid data for task_key %s, retrying: %r',
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400389 task_key, response)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700390 continue
391
392 # Swarming server uses non-empty 'output' value as a flag that task has
393 # finished. How to wait for tasks that produce no output is a mystery.
394 if result.get('output'):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700395 # Record the result, try to fetch attached output files (if any).
396 if output_collector:
397 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700398 output_collector.process_shard_result(shard_index, result)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700399 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000400
401
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700402def yield_results(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700403 swarm_base_url, task_keys, timeout, max_threads,
404 print_status_updates, output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500405 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000406
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700407 Duplicate shards are ignored. Shards are yielded in order of completion.
408 Timed out shards are NOT yielded at all. Caller can compare number of yielded
409 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000410
411 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500412 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 +0000413 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500414
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700415 output_collector is an optional instance of TaskOutputCollector that will be
416 used to fetch files produced by a task from isolate server to the local disk.
417
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500418 Yields:
419 (index, result). In particular, 'result' is defined as the
420 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000421 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000422 number_threads = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500423 min(max_threads, len(task_keys)) if max_threads else len(task_keys))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700424 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700425 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700426
maruel@chromium.org0437a732013-08-27 16:05:52 +0000427 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
428 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700429 # Adds a task to the thread pool to call 'retrieve_results' and return
430 # the results together with shard_index that produced them (as a tuple).
431 def enqueue_retrieve_results(shard_index, task_key):
432 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000433 pool.add_task(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700434 0, results_channel.wrap_task(task_fn),
435 swarm_base_url, shard_index, task_key, timeout,
436 should_stop, output_collector)
437
438 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
439 for shard_index, task_key in enumerate(task_keys):
440 enqueue_retrieve_results(shard_index, task_key)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700441
442 # Wait for all of them to finish.
443 shards_remaining = range(len(task_keys))
444 active_task_count = len(task_keys)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700445 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700446 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700447 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700448 shard_index, result = results_channel.pull(
449 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700450 except threading_utils.TaskChannel.Timeout:
451 if print_status_updates:
452 print(
453 'Waiting for results from the following shards: %s' %
454 ', '.join(map(str, shards_remaining)))
455 sys.stdout.flush()
456 continue
457 except Exception:
458 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700459
460 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700461 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000462 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500463 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000464 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700465
Vadim Shtayurab450c602014-05-12 19:23:25 -0700466 # Yield back results to the caller.
467 assert shard_index in shards_remaining
468 shards_remaining.remove(shard_index)
469 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700470
maruel@chromium.org0437a732013-08-27 16:05:52 +0000471 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700472 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000473 should_stop.set()
474
475
Vadim Shtayurab450c602014-05-12 19:23:25 -0700476def setup_run_isolated(manifest, bundle):
477 """Sets up the manifest to run an isolated task via run_isolated.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000478
Vadim Shtayurab450c602014-05-12 19:23:25 -0700479 Modifies |bundle| (by adding files) and |manifest| (by adding commands) in
480 place.
481
482 Args:
483 manifest: Manifest with swarm task definition.
484 bundle: ZipPackage with files that would be transfered to swarm bot.
485 If None, only |manifest| is modified (useful in tests).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000486 """
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000487 # Add uncompressed zip here. It'll be compressed as part of the package sent
488 # to Swarming server.
489 run_test_name = 'run_isolated.zip'
Vadim Shtayurab450c602014-05-12 19:23:25 -0700490 if bundle and run_test_name not in bundle.files:
491 bundle.add_buffer(
492 run_test_name,
493 run_isolated.get_as_zip_package().zip_into_buffer(compress=False))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000494
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000495 cleanup_script_name = 'swarm_cleanup.py'
Vadim Shtayurab450c602014-05-12 19:23:25 -0700496 if bundle and cleanup_script_name not in bundle.files:
497 bundle.add_file(
498 os.path.join(TOOLS_PATH, cleanup_script_name), cleanup_script_name)
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000499
maruel@chromium.org0437a732013-08-27 16:05:52 +0000500 run_cmd = [
501 'python', run_test_name,
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000502 '--hash', manifest.isolated_hash,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500503 '--namespace', manifest.namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000504 ]
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500505 if file_path.is_url(manifest.isolate_server):
506 run_cmd.extend(('--isolate-server', manifest.isolate_server))
507 else:
508 run_cmd.extend(('--indir', manifest.isolate_server))
509
maruel@chromium.org0437a732013-08-27 16:05:52 +0000510 if manifest.verbose or manifest.profile:
511 # Have it print the profiling section.
512 run_cmd.append('--verbose')
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700513
514 # Pass all extra args for run_isolated.py, it will pass them to the command.
515 if manifest.extra_args:
516 run_cmd.append('--')
517 run_cmd.extend(manifest.extra_args)
518
maruel@chromium.org0437a732013-08-27 16:05:52 +0000519 manifest.add_task('Run Test', run_cmd)
520
521 # Clean up
522 manifest.add_task('Clean Up', ['python', cleanup_script_name])
523
524
Vadim Shtayurab450c602014-05-12 19:23:25 -0700525def setup_googletest(env, shards, index):
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500526 """Sets googletest specific environment variables."""
527 if shards > 1:
528 env = env.copy()
Vadim Shtayurab450c602014-05-12 19:23:25 -0700529 env['GTEST_SHARD_INDEX'] = str(index)
530 env['GTEST_TOTAL_SHARDS'] = str(shards)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500531 return env
532
533
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500534def archive(isolate_server, namespace, isolated, algo, verbose):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000535 """Archives a .isolated and all the dependencies on the CAC."""
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500536 logging.info('archive(%s, %s, %s)', isolate_server, namespace, isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000537 tempdir = None
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500538 if file_path.is_url(isolate_server):
539 command = 'archive'
540 flag = '--isolate-server'
541 else:
542 command = 'hashtable'
543 flag = '--outdir'
544
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500545 print('Archiving: %s' % isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000546 try:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000547 cmd = [
548 sys.executable,
549 os.path.join(ROOT_DIR, 'isolate.py'),
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500550 command,
551 flag, isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500552 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000553 '--isolated', isolated,
554 ]
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000555 cmd.extend(['--verbose'] * verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000556 logging.info(' '.join(cmd))
557 if subprocess.call(cmd, verbose):
558 return
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400559 return isolated_format.hash_file(isolated, algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000560 finally:
561 if tempdir:
562 shutil.rmtree(tempdir)
563
564
Vadim Shtayurab450c602014-05-12 19:23:25 -0700565def get_shard_task_name(task_name, shards, index):
566 """Returns a task name to use for a single shard of a task."""
567 if shards == 1:
568 return task_name
569 return '%s:%s:%s' % (task_name, shards, index)
570
571
572def upload_zip_bundle(isolate_server, bundle):
573 """Uploads a zip package to isolate storage and returns raw fetch URL.
574
575 Args:
576 isolate_server: URL of an isolate server.
577 bundle: instance of ZipPackage to upload.
578
579 Returns:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400580 URL to get the file from.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700581 """
582 # Swarming bot would need to be able to grab the file from the storage
583 # using raw HTTP GET. Use 'default' namespace so that the raw data returned
584 # to a bot is not zipped, since swarm_bot doesn't understand compressed
585 # data yet. This namespace have nothing to do with |namespace| passed to
586 # run_isolated.py that is used to store files for isolated task.
587 logging.info('Zipping up and uploading files...')
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400588 start_time = now()
589 isolate_item = isolateserver.BufferItem(
590 bundle.zip_into_buffer(), high_priority=True)
591 with isolateserver.get_storage(isolate_server, 'default') as storage:
592 uploaded = storage.upload_items([isolate_item])
593 bundle_url = storage.get_fetch_url(isolate_item)
594 elapsed = now() - start_time
Vadim Shtayurab450c602014-05-12 19:23:25 -0700595 if isolate_item in uploaded:
596 logging.info('Upload complete, time elapsed: %f', elapsed)
597 else:
598 logging.info('Zip file already on server, time elapsed: %f', elapsed)
599 return bundle_url
600
601
602def trigger_by_manifest(swarming, manifest):
603 """Given a task manifest, triggers it for execution on swarming.
604
605 Args:
606 swarming: URL of a swarming service.
607 manifest: instance of Manifest.
608
609 Returns:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400610 tuple(Task id, priority) on success. tuple(None, None) on failure.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700611 """
612 logging.info('Triggering: %s', manifest.task_name)
613 manifest_text = manifest.to_json()
614 result = net.url_read(swarming + '/test', data={'request': manifest_text})
615 if not result:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400616 on_error.report('Failed to trigger task %s' % manifest.task_name)
Vadim Shtayura1c024f72014-07-09 19:00:10 -0700617 return None, None
Vadim Shtayurab450c602014-05-12 19:23:25 -0700618 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400619 data = json.loads(result)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400620 except (ValueError, TypeError):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700621 msg = '\n'.join((
622 'Failed to trigger task %s' % manifest.task_name,
623 'Manifest: %s' % manifest_text,
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400624 'Bad response: %s' % result))
625 on_error.report(msg)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400626 return None, None
627 if not data:
628 return None, None
629 return data['test_keys'][0]['test_key'], data['priority']
Vadim Shtayurab450c602014-05-12 19:23:25 -0700630
631
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400632def abort_task(_swarming, _manifest):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 """Given a task manifest that was triggered, aborts its execution."""
634 # TODO(vadimsh): No supported by the server yet.
635
636
637def trigger_task_shards(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700638 swarming, isolate_server, namespace, isolated_hash, task_name, extra_args,
Marc-Antoine Ruelaea50652014-06-12 14:23:48 -0400639 shards, dimensions, env, deadline, verbose, profile, priority):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400640 """Triggers multiple subtasks of a sharded task.
641
642 Returns:
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700643 Dict with task details, returned to caller as part of --dump-json output.
644 None in case of failure.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400645 """
Vadim Shtayurab450c602014-05-12 19:23:25 -0700646 # Collects all files that are necessary to bootstrap a task execution
647 # on the bot. Usually it includes self contained run_isolated.zip and
648 # a bunch of small other scripts. All heavy files are pulled
649 # by run_isolated.zip. Updated in 'setup_run_isolated'.
650 bundle = zip_package.ZipPackage(ROOT_DIR)
651
652 # Make a separate Manifest for each shard, put shard index and number of
653 # shards into env and subtask name.
654 manifests = []
655 for index in xrange(shards):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656 manifest = Manifest(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500657 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500658 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500659 isolated_hash=isolated_hash,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700660 task_name=get_shard_task_name(task_name, shards, index),
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700661 extra_args=extra_args,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500662 dimensions=dimensions,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700663 env=setup_googletest(env, shards, index),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400664 deadline=deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500665 verbose=verbose,
666 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800667 priority=priority)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700668 setup_run_isolated(manifest, bundle)
669 manifests.append(manifest)
670
671 # Upload zip bundle file to get its URL.
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400672 try:
673 bundle_url = upload_zip_bundle(isolate_server, bundle)
674 except (IOError, OSError):
675 on_error.report('Failed to upload the zip file for task %s' % task_name)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400676 return None, None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677
Vadim Shtayurab450c602014-05-12 19:23:25 -0700678 # Attach that file to all manifests.
679 for manifest in manifests:
680 manifest.add_bundled_file('swarm_data.zip', bundle_url)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681
Vadim Shtayurab450c602014-05-12 19:23:25 -0700682 # Trigger all the subtasks.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400683 tasks = {}
684 priority_warning = False
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700685 for index, manifest in enumerate(manifests):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400686 task_id, priority = trigger_by_manifest(swarming, manifest)
687 if not task_id:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700688 break
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400689 if not priority_warning and priority != manifest.priority:
690 priority_warning = True
691 print >> sys.stderr, 'Priority was reset to %s' % priority
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700692 tasks[manifest.task_name] = {
693 'shard_index': index,
694 'task_id': task_id,
695 'view_url': '%s/user/task/%s' % (swarming, task_id),
696 }
Vadim Shtayurab450c602014-05-12 19:23:25 -0700697
698 # Some shards weren't triggered. Abort everything.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400699 if len(tasks) != len(manifests):
700 if tasks:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700701 print >> sys.stderr, 'Not all shards were triggered'
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700702 for task_dict in tasks.itervalues():
703 abort_task(swarming, task_dict['task_id'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400704 return None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000705
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400706 return tasks
maruel@chromium.org0437a732013-08-27 16:05:52 +0000707
708
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500709def isolated_to_hash(isolate_server, namespace, arg, algo, verbose):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500710 """Archives a .isolated file if needed.
711
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500712 Returns the file hash to trigger and a bool specifying if it was a file (True)
713 or a hash (False).
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500714 """
715 if arg.endswith('.isolated'):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500716 file_hash = archive(isolate_server, namespace, arg, algo, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500717 if not file_hash:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400718 on_error.report('Archival failure %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500719 return None, True
720 return file_hash, True
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400721 elif isolated_format.is_valid_hash(arg, algo):
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500722 return arg, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500723 else:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400724 on_error.report('Invalid hash %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500725 return None, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500726
727
maruel@chromium.org0437a732013-08-27 16:05:52 +0000728def trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500729 swarming,
730 isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500731 namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500732 file_hash_or_isolated,
733 task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700734 extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500735 shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500736 dimensions,
737 env,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400738 deadline,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000739 verbose,
740 profile,
741 priority):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400742 """Sends off the hash swarming task requests.
743
744 Returns:
745 tuple(dict(task_name: task_id), base task name). The dict of tasks is None
746 in case of failure.
747 """
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500748 file_hash, is_file = isolated_to_hash(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500749 isolate_server, namespace, file_hash_or_isolated, hashlib.sha1, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500750 if not file_hash:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500751 return 1, ''
752 if not task_name:
753 # If a file name was passed, use its base name of the isolated hash.
754 # Otherwise, use user name as an approximation of a task name.
755 if is_file:
756 key = os.path.splitext(os.path.basename(file_hash_or_isolated))[0]
757 else:
758 key = getpass.getuser()
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700759 task_name = '%s/%s/%s/%d' % (
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500760 key,
761 '_'.join('%s=%s' % (k, v) for k, v in sorted(dimensions.iteritems())),
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700762 file_hash,
763 now() * 1000)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500764
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400765 tasks = trigger_task_shards(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500766 swarming=swarming,
767 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500768 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500769 isolated_hash=file_hash,
770 task_name=task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700771 extra_args=extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500772 shards=shards,
773 dimensions=dimensions,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400774 deadline=deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500775 env=env,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500776 verbose=verbose,
777 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800778 priority=priority)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400779 return tasks, task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +0000780
781
Vadim Shtayurab450c602014-05-12 19:23:25 -0700782def decorate_shard_output(shard_index, result, shard_exit_code):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000783 """Returns wrapped output for swarming task shard."""
784 tag = 'index %s (machine tag: %s, id: %s)' % (
Vadim Shtayurab450c602014-05-12 19:23:25 -0700785 shard_index,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000786 result['machine_id'],
787 result.get('machine_tag', 'unknown'))
788 return (
789 '\n'
790 '================================================================\n'
791 'Begin output from shard %s\n'
792 '================================================================\n'
793 '\n'
794 '%s'
795 '================================================================\n'
Vadim Shtayura473455a2014-05-14 15:22:35 -0700796 'End output from shard %s.\nExit code %d (%s).\n'
797 '================================================================\n') % (
798 tag, result['output'] or NO_OUTPUT_FOUND, tag,
799 shard_exit_code, hex(0xffffffff & shard_exit_code))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000800
801
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700802def collect(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700803 url, task_name, shards, timeout, decorate,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700804 print_status_updates, task_summary_json, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500805 """Retrieves results of a Swarming task."""
Vadim Shtayurab450c602014-05-12 19:23:25 -0700806 # Grab task keys for each shard. Order is important, used to figure out
807 # shard index based on the key.
808 # TODO(vadimsh): Simplify this once server support is added.
809 task_keys = []
810 for index in xrange(shards):
811 shard_task_name = get_shard_task_name(task_name, shards, index)
812 logging.info('Collecting %s', shard_task_name)
813 shard_task_keys = get_task_keys(url, shard_task_name)
814 if not shard_task_keys:
815 raise Failure('No task keys to get results with: %s' % shard_task_name)
816 if len(shard_task_keys) != 1:
817 raise Failure('Expecting only one shard for a task: %s' % shard_task_name)
818 task_keys.append(shard_task_keys[0])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000819
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700820 # Collect summary JSON and output files (if task_output_dir is not None).
821 output_collector = TaskOutputCollector(
822 task_output_dir, task_name, len(task_keys))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700823
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700824 seen_shards = set()
Vadim Shtayurac524f512014-05-15 09:54:56 -0700825 exit_codes = []
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700826
827 try:
828 for index, output in yield_results(
829 url, task_keys, timeout, None, print_status_updates, output_collector):
830 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700831
832 # Grab first non-zero exit code as an overall shard exit code.
833 shard_exit_code = 0
834 for code in map(int, (output['exit_codes'] or '1').split(',')):
835 if code:
836 shard_exit_code = code
837 break
Vadim Shtayurac524f512014-05-15 09:54:56 -0700838 exit_codes.append(shard_exit_code)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700839
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700840 if decorate:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700841 print decorate_shard_output(index, output, shard_exit_code)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700842 else:
843 print(
844 '%s/%s: %s' % (
845 output['machine_id'],
846 output['machine_tag'],
847 output['exit_codes']))
848 print(''.join(' %s\n' % l for l in output['output'].splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700849 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700850 summary = output_collector.finalize()
851 if task_summary_json:
852 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700853
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700854 if len(seen_shards) != len(task_keys):
855 missing_shards = [x for x in range(len(task_keys)) if x not in seen_shards]
856 print >> sys.stderr, ('Results from some shards are missing: %s' %
857 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700858 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700859
Vadim Shtayurac524f512014-05-15 09:54:56 -0700860 return int(bool(any(exit_codes)))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000861
862
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400863def add_filter_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500864 parser.filter_group = tools.optparse.OptionGroup(parser, 'Filtering slaves')
865 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500866 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500867 dest='dimensions', metavar='FOO bar',
868 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500869 parser.add_option_group(parser.filter_group)
870
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400871
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400872def process_filter_options(parser, options):
873 options.dimensions = dict(options.dimensions)
874 if not options.dimensions:
875 parser.error('Please at least specify one --dimension')
876
877
Vadim Shtayurab450c602014-05-12 19:23:25 -0700878def add_sharding_options(parser):
879 parser.sharding_group = tools.optparse.OptionGroup(parser, 'Sharding options')
880 parser.sharding_group.add_option(
881 '--shards', type='int', default=1,
882 help='Number of shards to trigger and collect.')
883 parser.add_option_group(parser.sharding_group)
884
885
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400886def add_trigger_options(parser):
887 """Adds all options to trigger a task on Swarming."""
888 isolateserver.add_isolate_server_options(parser, True)
889 add_filter_options(parser)
890
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500891 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
892 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500893 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700894 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500895 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500896 '--priority', type='int', default=100,
897 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500898 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500899 '-T', '--task-name',
900 help='Display name of the task. It uniquely identifies the task. '
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700901 'Defaults to <base_name>/<dimensions>/<isolated hash>/<timestamp> '
902 'if an isolated file is provided, if a hash is provided, it '
903 'defaults to <user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400904 parser.task_group.add_option(
905 '--deadline', type='int', default=6*60*60,
906 help='Seconds to allow the task to be pending for a bot to run before '
907 'this task request expires.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500908 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500909 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500910 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000911 '--profile', action='store_true',
912 default=bool(os.environ.get('ISOLATE_DEBUG')),
913 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000914
915
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500916def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500917 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500918 if len(args) != 1:
919 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400920 process_filter_options(parser, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000921
922
923def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500924 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000925 '-t', '--timeout',
926 type='float',
927 default=DEFAULT_SHARD_WAIT_TIME,
928 help='Timeout to wait for result, set to 0 for no timeout; default: '
929 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500930 parser.group_logging.add_option(
931 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700932 parser.group_logging.add_option(
933 '--print-status-updates', action='store_true',
934 help='Print periodic status updates')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700935 parser.task_output_group = tools.optparse.OptionGroup(parser, 'Task output')
936 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700937 '--task-summary-json',
938 metavar='FILE',
939 help='Dump a summary of task results to this file as json. It contains '
940 'only shards statuses as know to server directly. Any output files '
941 'emitted by the task can be collected by using --task-output-dir')
942 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700943 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700944 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700945 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700946 'directory contains per-shard directory with output files produced '
947 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700948 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000949
950
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700951def extract_isolated_command_extra_args(args):
952 try:
953 index = args.index('--')
954 except ValueError:
955 return (args, [])
956 return (args[:index], args[index+1:])
957
958
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400959def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400960 """Returns information about the bots connected to the Swarming server."""
961 add_filter_options(parser)
962 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400963 '--dead-only', action='store_true',
964 help='Only print dead bots, useful to reap them and reimage broken bots')
965 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400966 '-k', '--keep-dead', action='store_true',
967 help='Do not filter out dead bots')
968 parser.filter_group.add_option(
969 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400970 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400971 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400972
973 if options.keep_dead and options.dead_only:
974 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700975
976 auth.ensure_logged_in(options.swarming)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400977
978 bots = []
979 cursor = None
980 limit = 250
981 # Iterate via cursors.
982 base_url = options.swarming + '/swarming/api/v1/client/bots?limit=%d' % limit
983 while True:
984 url = base_url
985 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400986 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400987 data = net.url_read_json(url)
988 if data is None:
989 print >> sys.stderr, 'Failed to access %s' % options.swarming
990 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400991 bots.extend(data['items'])
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400992 cursor = data['cursor']
993 if not cursor:
994 break
995
996 for bot in natsort.natsorted(bots, key=lambda x: x['id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400997 if options.dead_only:
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400998 if not bot['is_dead']:
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400999 continue
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001000 elif not options.keep_dead and bot['is_dead']:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001001 continue
1002
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001003 # If the user requested to filter on dimensions, ensure the bot has all the
1004 # dimensions requested.
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001005 dimensions = bot['dimensions']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001006 for key, value in options.dimensions:
1007 if key not in dimensions:
1008 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001009 # A bot can have multiple value for a key, for example,
1010 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1011 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001012 if isinstance(dimensions[key], list):
1013 if value not in dimensions[key]:
1014 break
1015 else:
1016 if value != dimensions[key]:
1017 break
1018 else:
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001019 print bot['id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001020 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001021 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001022 if bot['task']:
1023 print ' task: %s' % bot['task']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001024 return 0
1025
1026
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001027@subcommand.usage('task_name')
1028def CMDcollect(parser, args):
1029 """Retrieves results of a Swarming task.
1030
1031 The result can be in multiple part if the execution was sharded. It can
1032 potentially have retries.
1033 """
1034 add_collect_options(parser)
1035 add_sharding_options(parser)
1036 (options, args) = parser.parse_args(args)
1037 if not args:
1038 parser.error('Must specify one task name.')
1039 elif len(args) > 1:
1040 parser.error('Must specify only one task name.')
1041
1042 auth.ensure_logged_in(options.swarming)
1043 try:
1044 return collect(
1045 options.swarming,
1046 args[0],
1047 options.shards,
1048 options.timeout,
1049 options.decorate,
1050 options.print_status_updates,
1051 options.task_summary_json,
1052 options.task_output_dir)
1053 except Failure:
1054 on_error.report(None)
1055 return 1
1056
1057
1058@subcommand.usage('[resource name]')
1059def CMDquery(parser, args):
1060 """Returns raw JSON information via an URL endpoint. Use 'list' to gather the
1061 list of valid values from the server.
1062
1063 Examples:
1064 Printing the list of known URLs:
1065 swarming.py query -S https://server-url list
1066
1067 Listing last 50 tasks on a specific bot named 'swarm1'
1068 swarming.py query -S https://server-url --limit 50 bot/swarm1/tasks
1069 """
1070 CHUNK_SIZE = 250
1071
1072 parser.add_option(
1073 '-L', '--limit', type='int', default=200,
1074 help='Limit to enforce on limitless items (like number of tasks); '
1075 'default=%default')
1076 (options, args) = parser.parse_args(args)
1077 if len(args) != 1:
1078 parser.error('Must specify only one resource name.')
1079
1080 auth.ensure_logged_in(options.swarming)
1081
1082 base_url = options.swarming + '/swarming/api/v1/client/' + args[0]
1083 url = base_url
1084 if options.limit:
1085 url += '?limit=%d' % min(CHUNK_SIZE, options.limit)
1086 data = net.url_read_json(url)
1087 if data is None:
1088 print >> sys.stderr, 'Failed to access %s' % options.swarming
1089 return 1
1090
1091 # Some items support cursors. Try to get automatically if cursors are needed
1092 # by looking at the 'cursor' items.
1093 while (
1094 data.get('cursor') and
1095 (not options.limit or len(data['items']) < options.limit)):
1096 url = base_url + '?cursor=%s' % urllib.quote(data['cursor'])
1097 if options.limit:
1098 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
1099 new = net.url_read_json(url)
1100 if new is None:
1101 print >> sys.stderr, 'Failed to access %s' % options.swarming
1102 return 1
1103 data['items'].extend(new['items'])
1104 data['cursor'] = new['cursor']
1105
1106 if options.limit and len(data.get('items', [])) > options.limit:
1107 data['items'] = data['items'][:options.limit]
1108 data.pop('cursor', None)
1109
1110 json.dump(data, sys.stdout, indent=2, sort_keys=True)
1111 sys.stdout.write('\n')
1112 return 0
1113
1114
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001115@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001116def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001117 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001118
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001119 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001120 """
1121 add_trigger_options(parser)
1122 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001123 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001124 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001125 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001126 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001127
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001128 auth.ensure_logged_in(options.swarming)
1129 if file_path.is_url(options.isolate_server):
1130 auth.ensure_logged_in(options.isolate_server)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001131 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001132 tasks, task_name = trigger(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001133 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001134 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001135 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001136 file_hash_or_isolated=args[0],
1137 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001138 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001139 shards=options.shards,
1140 dimensions=options.dimensions,
1141 env=dict(options.env),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -04001142 deadline=options.deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001143 verbose=options.verbose,
1144 profile=options.profile,
1145 priority=options.priority)
1146 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001147 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001148 'Failed to trigger %s(%s): %s' %
1149 (options.task_name, args[0], e.args[0]))
1150 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001151 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001152 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001153 return 1
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001154 if task_name != options.task_name:
1155 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001156 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001157 # TODO(maruel): Use task_ids, it's much more efficient!
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001158 return collect(
1159 options.swarming,
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001160 task_name,
Vadim Shtayurab450c602014-05-12 19:23:25 -07001161 options.shards,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001162 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001163 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001164 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001165 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001166 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001167 except Failure:
1168 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001169 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001170
1171
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001172@subcommand.usage("(hash|isolated) [-- extra_args]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001173def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001174 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001175
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001176 Accepts either the hash (sha1) of a .isolated file already uploaded or the
1177 path to an .isolated file to archive, packages it if needed and sends a
1178 Swarming manifest file to the Swarming server.
1179
1180 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001181
1182 Passes all extra arguments provided after '--' as additional command line
1183 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001184 """
1185 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001186 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001187 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001188 parser.add_option(
1189 '--dump-json',
1190 metavar='FILE',
1191 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001192 options, args = parser.parse_args(args)
1193 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001194
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001195 auth.ensure_logged_in(options.swarming)
1196 if file_path.is_url(options.isolate_server):
1197 auth.ensure_logged_in(options.isolate_server)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001198 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001199 tasks, task_name = trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001200 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001201 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001202 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001203 file_hash_or_isolated=args[0],
1204 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001205 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001206 shards=options.shards,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001207 dimensions=options.dimensions,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -05001208 env=dict(options.env),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -04001209 deadline=options.deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001210 verbose=options.verbose,
1211 profile=options.profile,
1212 priority=options.priority)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001213 if tasks:
1214 if task_name != options.task_name:
1215 print('Triggered task: %s' % task_name)
1216 if options.dump_json:
1217 data = {
1218 'base_task_name': task_name,
1219 'tasks': tasks,
1220 }
1221 tools.write_json(options.dump_json, data, True)
1222 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001223 except Failure:
1224 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001225 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001226
1227
1228class OptionParserSwarming(tools.OptionParserWithLogging):
1229 def __init__(self, **kwargs):
1230 tools.OptionParserWithLogging.__init__(
1231 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001232 self.server_group = tools.optparse.OptionGroup(self, 'Server')
1233 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001234 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001235 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001236 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001237 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001238 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001239
1240 def parse_args(self, *args, **kwargs):
1241 options, args = tools.OptionParserWithLogging.parse_args(
1242 self, *args, **kwargs)
1243 options.swarming = options.swarming.rstrip('/')
1244 if not options.swarming:
1245 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001246 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001247 return options, args
1248
1249
1250def main(args):
1251 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001252 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001253
1254
1255if __name__ == '__main__':
1256 fix_encoding.fix_encoding()
1257 tools.disable_buffering()
1258 colorama.init()
1259 sys.exit(main(sys.argv[1:]))