blob: 9a8f2ef85823094d58905552e01f5c3ef37e639c [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 Ruel8806e622014-02-12 14:15:53 -05008__version__ = '0.4.1'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
10import hashlib
11import json
12import logging
13import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000014import shutil
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import subprocess
16import sys
17import time
18import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000019
20from third_party import colorama
21from third_party.depot_tools import fix_encoding
22from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000023
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050024from utils import file_path
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000025from utils import net
maruel@chromium.org0437a732013-08-27 16:05:52 +000026from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000027from utils import tools
28from utils import zip_package
maruel@chromium.org0437a732013-08-27 16:05:52 +000029
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080030import auth
maruel@chromium.org7b844a62013-09-17 13:04:59 +000031import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000032import run_isolated
33
34
35ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
36TOOLS_PATH = os.path.join(ROOT_DIR, 'tools')
37
38
maruel@chromium.org0437a732013-08-27 16:05:52 +000039# The default time to wait for a shard to finish running.
csharp@chromium.org24758492013-08-28 19:10:54 +000040DEFAULT_SHARD_WAIT_TIME = 80 * 60.
maruel@chromium.org0437a732013-08-27 16:05:52 +000041
42
43NO_OUTPUT_FOUND = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050044 'No output produced by the task, it may have failed to run.\n'
maruel@chromium.org0437a732013-08-27 16:05:52 +000045 '\n')
46
47
maruel@chromium.org0437a732013-08-27 16:05:52 +000048class Failure(Exception):
49 """Generic failure."""
50 pass
51
52
53class Manifest(object):
54 """Represents a Swarming task manifest.
55
56 Also includes code to zip code and upload itself.
57 """
58 def __init__(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050059 self, isolate_server, namespace, isolated_hash, task_name, shards, env,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050060 dimensions, working_dir, verbose, profile, priority, algo):
maruel@chromium.org0437a732013-08-27 16:05:52 +000061 """Populates a manifest object.
62 Args:
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050063 isolate_server - isolate server url.
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050064 namespace - isolate server namespace to use.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000065 isolated_hash - The manifest's sha-1 that the slave is going to fetch.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050066 task_name - The name to give the task request.
67 shards - The number of swarming shards to request.
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -050068 env - environment variables to set.
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050069 dimensions - dimensions to filter the task on.
maruel@chromium.org0437a732013-08-27 16:05:52 +000070 working_dir - Relative working directory to start the script.
maruel@chromium.org0437a732013-08-27 16:05:52 +000071 verbose - if True, have the slave print more details.
72 profile - if True, have the slave print more timing data.
maruel@chromium.org7b844a62013-09-17 13:04:59 +000073 priority - int between 0 and 1000, lower the higher priority.
74 algo - hashing algorithm used.
maruel@chromium.org0437a732013-08-27 16:05:52 +000075 """
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050076 self.isolate_server = isolate_server
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050077 self.namespace = namespace
78 # The reason is that swarm_bot doesn't understand compressed data yet. So
79 # the data to be downloaded by swarm_bot is in 'default', independent of
80 # what run_isolated.py is going to fetch.
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050081 self.storage = isolateserver.get_storage(isolate_server, 'default')
82
maruel@chromium.org814d23f2013-10-01 19:08:00 +000083 self.isolated_hash = isolated_hash
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000084 self.bundle = zip_package.ZipPackage(ROOT_DIR)
85
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050086 self._task_name = task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +000087 self._shards = shards
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050088 self._env = env.copy()
89 self._dimensions = dimensions.copy()
maruel@chromium.org0437a732013-08-27 16:05:52 +000090 self._working_dir = working_dir
91
maruel@chromium.org0437a732013-08-27 16:05:52 +000092 self.verbose = bool(verbose)
93 self.profile = bool(profile)
94 self.priority = priority
maruel@chromium.org7b844a62013-09-17 13:04:59 +000095 self._algo = algo
maruel@chromium.org0437a732013-08-27 16:05:52 +000096
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +000097 self._isolate_item = None
maruel@chromium.org0437a732013-08-27 16:05:52 +000098 self._tasks = []
maruel@chromium.org0437a732013-08-27 16:05:52 +000099
100 def add_task(self, task_name, actions, time_out=600):
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500101 """Appends a new task to the swarming manifest file.
102
103 Tasks cannot be added once the manifest was uploaded.
104 """
105 assert not self._isolate_item
maruel@chromium.org0437a732013-08-27 16:05:52 +0000106 # See swarming/src/common/test_request_message.py TestObject constructor for
107 # the valid flags.
108 self._tasks.append(
109 {
110 'action': actions,
111 'decorate_output': self.verbose,
112 'test_name': task_name,
113 'time_out': time_out,
114 })
115
maruel@chromium.org0437a732013-08-27 16:05:52 +0000116 def to_json(self):
117 """Exports the current configuration into a swarm-readable manifest file.
118
119 This function doesn't mutate the object.
120 """
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500121 request = {
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500122 'cleanup': 'root',
maruel@chromium.org0437a732013-08-27 16:05:52 +0000123 'configurations': [
124 {
Marc-Antoine Ruel5d799192013-11-06 15:20:39 -0500125 'config_name': 'isolated',
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500126 'dimensions': self._dimensions,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500127 'min_instances': self._shards,
128 'priority': self.priority,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000129 },
130 ],
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500131 'data': [],
132 # TODO: Let the encoding get set from the command line.
133 'encoding': 'UTF-8',
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -0500134 'env_vars': self._env,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000135 'restart_on_failure': True,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500136 'test_case_name': self._task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500137 'tests': self._tasks,
138 'working_dir': self._working_dir,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000139 }
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000140 if self._isolate_item:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500141 request['data'].append(
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000142 [
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000143 self.storage.get_fetch_url(self._isolate_item.digest),
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000144 'swarm_data.zip',
145 ])
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500146 return json.dumps(request, sort_keys=True, separators=(',',':'))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000147
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500148 @property
149 def isolate_item(self):
150 """Calling this property 'closes' the manifest and it can't be modified
151 afterward.
152 """
153 if self._isolate_item is None:
154 self._isolate_item = isolateserver.BufferItem(
155 self.bundle.zip_into_buffer(), self._algo, is_isolated=True)
156 return self._isolate_item
157
158
159def zip_and_upload(manifest):
160 """Zips up all the files necessary to run a manifest and uploads to Swarming
161 master.
162 """
163 try:
164 start_time = time.time()
165 with manifest.storage:
166 uploaded = manifest.storage.upload_items([manifest.isolate_item])
167 elapsed = time.time() - start_time
168 except (IOError, OSError) as exc:
169 tools.report_error('Failed to upload the zip file: %s' % exc)
170 return False
171
172 if manifest.isolate_item in uploaded:
173 logging.info('Upload complete, time elapsed: %f', elapsed)
174 else:
175 logging.info('Zip file already on server, time elapsed: %f', elapsed)
176 return True
177
maruel@chromium.org0437a732013-08-27 16:05:52 +0000178
179def now():
180 """Exists so it can be mocked easily."""
181 return time.time()
182
183
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500184def get_task_keys(swarm_base_url, task_name):
185 """Returns the Swarming task key for each shards of task_name."""
186 key_data = urllib.urlencode([('name', task_name)])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000187 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data)
188
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000189 for _ in net.retry_loop(max_attempts=net.URL_OPEN_MAX_ATTEMPTS):
190 result = net.url_read(url, retry_404=True)
191 if result is None:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000192 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500193 'Error: Unable to find any task with the name, %s, on swarming server'
194 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000195
maruel@chromium.org0437a732013-08-27 16:05:52 +0000196 # TODO(maruel): Compare exact string.
197 if 'No matching' in result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500198 logging.warning('Unable to find any task with the name, %s, on swarming '
199 'server' % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000200 continue
201 return json.loads(result)
202
203 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500204 'Error: Unable to find any task with the name, %s, on swarming server'
205 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000206
207
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500208def retrieve_results(base_url, task_key, timeout, should_stop):
209 """Retrieves results for a single task_key."""
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000210 assert isinstance(timeout, float), timeout
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500211 params = [('r', task_key)]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000212 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params))
213 start = now()
214 while True:
215 if timeout and (now() - start) >= timeout:
216 logging.error('retrieve_results(%s) timed out', base_url)
217 return {}
218 # Do retries ourselves.
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000219 response = net.url_read(result_url, retry_404=False, retry_50x=False)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000220 if response is None:
221 # Aggressively poll for results. Do not use retry_404 so
222 # should_stop is polled more often.
223 remaining = min(5, timeout - (now() - start)) if timeout else 5
224 if remaining > 0:
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000225 if should_stop.get():
226 return {}
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000227 net.sleep_before_retry(1, remaining)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000228 else:
229 try:
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000230 data = json.loads(response) or {}
maruel@chromium.org0437a732013-08-27 16:05:52 +0000231 except (ValueError, TypeError):
232 logging.warning(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500233 'Received corrupted data for task_key %s. Retrying.', task_key)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000234 else:
235 if data['output']:
236 return data
237 if should_stop.get():
238 return {}
239
240
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500241def yield_results(swarm_base_url, task_keys, timeout, max_threads):
242 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000243
244 Duplicate shards are ignored, the first one to complete is returned.
245
246 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500247 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 +0000248 worth normally to limit the number threads. Mostly used for testing purposes.
249 """
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500250 shards_remaining = range(len(task_keys))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000251 number_threads = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500252 min(max_threads, len(task_keys)) if max_threads else len(task_keys))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000253 should_stop = threading_utils.Bit()
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500254 results_remaining = len(task_keys)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000255 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
256 try:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500257 for task_key in task_keys:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000258 pool.add_task(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500259 0, retrieve_results, swarm_base_url, task_key, timeout, should_stop)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000260 while shards_remaining and results_remaining:
261 result = pool.get_one_result()
262 results_remaining -= 1
263 if not result:
264 # Failed to retrieve one key.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500265 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000266 continue
267 shard_index = result['config_instance_index']
268 if shard_index in shards_remaining:
269 shards_remaining.remove(shard_index)
270 yield shard_index, result
271 else:
272 logging.warning('Ignoring duplicate shard index %d', shard_index)
273 # Pop the last entry, there's no such shard.
274 shards_remaining.pop()
275 finally:
276 # Done, kill the remaining threads.
277 should_stop.set()
278
279
280def chromium_setup(manifest):
281 """Sets up the commands to run.
282
283 Highly chromium specific.
284 """
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000285 # Add uncompressed zip here. It'll be compressed as part of the package sent
286 # to Swarming server.
287 run_test_name = 'run_isolated.zip'
288 manifest.bundle.add_buffer(run_test_name,
289 run_isolated.get_as_zip_package().zip_into_buffer(compress=False))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000290
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000291 cleanup_script_name = 'swarm_cleanup.py'
292 manifest.bundle.add_file(os.path.join(TOOLS_PATH, cleanup_script_name),
293 cleanup_script_name)
294
maruel@chromium.org0437a732013-08-27 16:05:52 +0000295 run_cmd = [
296 'python', run_test_name,
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000297 '--hash', manifest.isolated_hash,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500298 '--namespace', manifest.namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000299 ]
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500300 if file_path.is_url(manifest.isolate_server):
301 run_cmd.extend(('--isolate-server', manifest.isolate_server))
302 else:
303 run_cmd.extend(('--indir', manifest.isolate_server))
304
maruel@chromium.org0437a732013-08-27 16:05:52 +0000305 if manifest.verbose or manifest.profile:
306 # Have it print the profiling section.
307 run_cmd.append('--verbose')
308 manifest.add_task('Run Test', run_cmd)
309
310 # Clean up
311 manifest.add_task('Clean Up', ['python', cleanup_script_name])
312
313
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500314def googletest_setup(env, shards):
315 """Sets googletest specific environment variables."""
316 if shards > 1:
317 env = env.copy()
318 env['GTEST_SHARD_INDEX'] = '%(instance_index)s'
319 env['GTEST_TOTAL_SHARDS'] = '%(num_instances)s'
320 return env
321
322
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500323def archive(isolate_server, namespace, isolated, algo, verbose):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000324 """Archives a .isolated and all the dependencies on the CAC."""
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500325 logging.info('archive(%s, %s, %s)', isolate_server, namespace, isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000326 tempdir = None
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500327 if file_path.is_url(isolate_server):
328 command = 'archive'
329 flag = '--isolate-server'
330 else:
331 command = 'hashtable'
332 flag = '--outdir'
333
maruel@chromium.org0437a732013-08-27 16:05:52 +0000334 try:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000335 cmd = [
336 sys.executable,
337 os.path.join(ROOT_DIR, 'isolate.py'),
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500338 command,
339 flag, isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500340 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000341 '--isolated', isolated,
342 ]
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000343 cmd.extend(['--verbose'] * verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000344 logging.info(' '.join(cmd))
345 if subprocess.call(cmd, verbose):
346 return
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000347 return isolateserver.hash_file(isolated, algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000348 finally:
349 if tempdir:
350 shutil.rmtree(tempdir)
351
352
353def process_manifest(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500354 swarming, isolate_server, namespace, isolated_hash, task_name, shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500355 dimensions, env, working_dir, verbose, profile, priority, algo):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500356 """Processes the manifest file and send off the swarming task request."""
maruel@chromium.org0437a732013-08-27 16:05:52 +0000357 try:
358 manifest = Manifest(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500359 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500360 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500361 isolated_hash=isolated_hash,
362 task_name=task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500363 shards=shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500364 dimensions=dimensions,
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -0500365 env=env,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500366 working_dir=working_dir,
367 verbose=verbose,
368 profile=profile,
369 priority=priority,
370 algo=algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000371 except ValueError as e:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500372 tools.report_error('Unable to process %s: %s' % (task_name, e))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000373 return 1
374
375 chromium_setup(manifest)
376
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500377 logging.info('Zipping up files...')
378 if not zip_and_upload(manifest):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000379 return 1
380
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500381 logging.info('Server: %s', swarming)
382 logging.info('Task name: %s', task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500383 trigger_url = swarming + '/test'
maruel@chromium.org0437a732013-08-27 16:05:52 +0000384 manifest_text = manifest.to_json()
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500385 result = net.url_read(trigger_url, data={'request': manifest_text})
maruel@chromium.org0437a732013-08-27 16:05:52 +0000386 if not result:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000387 tools.report_error(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500388 'Failed to trigger task %s\n%s' % (task_name, trigger_url))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000389 return 1
390 try:
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000391 json.loads(result)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000392 except (ValueError, TypeError) as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000393 msg = '\n'.join((
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500394 'Failed to trigger task %s' % task_name,
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000395 'Manifest: %s' % manifest_text,
396 'Bad response: %s' % result,
397 str(e)))
398 tools.report_error(msg)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000399 return 1
400 return 0
401
402
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500403def isolated_to_hash(isolate_server, namespace, arg, algo, verbose):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500404 """Archives a .isolated file if needed.
405
406 Returns the file hash to trigger.
407 """
408 if arg.endswith('.isolated'):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500409 file_hash = archive(isolate_server, namespace, arg, algo, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500410 if not file_hash:
411 tools.report_error('Archival failure %s' % arg)
412 return None
413 return file_hash
414 elif isolateserver.is_valid_hash(arg, algo):
415 return arg
416 else:
417 tools.report_error('Invalid hash %s' % arg)
418 return None
419
420
maruel@chromium.org0437a732013-08-27 16:05:52 +0000421def trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500422 swarming,
423 isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500424 namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500425 file_hash_or_isolated,
426 task_name,
427 shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500428 dimensions,
429 env,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500430 working_dir,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000431 verbose,
432 profile,
433 priority):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500434 """Sends off the hash swarming task requests."""
435 file_hash = isolated_to_hash(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500436 isolate_server, namespace, file_hash_or_isolated, hashlib.sha1, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500437 if not file_hash:
438 return 1
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500439 env = googletest_setup(env, shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500440 # TODO(maruel): It should first create a request manifest object, then pass
441 # it to a function to zip, archive and trigger.
442 return process_manifest(
443 swarming=swarming,
444 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500445 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500446 isolated_hash=file_hash,
447 task_name=task_name,
448 shards=shards,
449 dimensions=dimensions,
450 env=env,
451 working_dir=working_dir,
452 verbose=verbose,
453 profile=profile,
454 priority=priority,
455 algo=hashlib.sha1)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000456
457
458def decorate_shard_output(result, shard_exit_code):
459 """Returns wrapped output for swarming task shard."""
460 tag = 'index %s (machine tag: %s, id: %s)' % (
461 result['config_instance_index'],
462 result['machine_id'],
463 result.get('machine_tag', 'unknown'))
464 return (
465 '\n'
466 '================================================================\n'
467 'Begin output from shard %s\n'
468 '================================================================\n'
469 '\n'
470 '%s'
471 '================================================================\n'
472 'End output from shard %s. Return %d\n'
473 '================================================================\n'
474 ) % (tag, result['output'] or NO_OUTPUT_FOUND, tag, shard_exit_code)
475
476
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500477def collect(url, task_name, timeout, decorate):
478 """Retrieves results of a Swarming task."""
479 logging.info('Collecting %s', task_name)
480 task_keys = get_task_keys(url, task_name)
481 if not task_keys:
482 raise Failure('No task keys to get results with.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000483
maruel@chromium.org9c1c7b52013-08-28 19:04:36 +0000484 exit_code = None
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500485 for _index, output in yield_results(url, task_keys, timeout, None):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000486 shard_exit_codes = (output['exit_codes'] or '1').split(',')
487 shard_exit_code = max(int(i) for i in shard_exit_codes)
488 if decorate:
489 print decorate_shard_output(output, shard_exit_code)
490 else:
491 print(
492 '%s/%s: %s' % (
493 output['machine_id'],
494 output['machine_tag'],
495 output['exit_codes']))
496 print(''.join(' %s\n' % l for l in output['output'].splitlines()))
maruel@chromium.org9c1c7b52013-08-28 19:04:36 +0000497 exit_code = exit_code or shard_exit_code
498 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000499
500
501def add_trigger_options(parser):
502 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500503 isolateserver.add_isolate_server_options(parser, True)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500504
505 parser.filter_group = tools.optparse.OptionGroup(parser, 'Filtering slaves')
506 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500507 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500508 dest='dimensions', metavar='FOO bar',
509 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500510 parser.add_option_group(parser.filter_group)
511
512 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
513 parser.task_group.add_option(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500514 '-w', '--working-dir', default='swarm_tests',
515 help='Working directory on the swarming slave side. default: %default.')
516 parser.task_group.add_option(
517 '--working_dir', help=tools.optparse.SUPPRESS_HELP)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500518 parser.task_group.add_option(
519 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
520 help='environment variables to set')
521 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500522 '--priority', type='int', default=100,
523 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500524 parser.task_group.add_option(
525 '--shards', type='int', default=1, help='number of shards to use')
526 parser.task_group.add_option(
527 '-T', '--task-name', help='display name of the task')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500528 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500529 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500530 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000531 '--profile', action='store_true',
532 default=bool(os.environ.get('ISOLATE_DEBUG')),
533 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000534
535
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500536def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500537 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel4d8093d2014-01-31 15:07:11 -0500538 if not options.task_name:
539 parser.error(
540 '--task-name is required. It should be <base_name>/<OS>/<isolated>')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500541 if len(args) != 1:
542 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500543 options.dimensions = dict(options.dimensions)
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500544 if not options.dimensions.get('os'):
545 parser.error(
546 'Please at least specify the dimension of the swarming bot OS with '
547 '--dimension os <something>.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000548
549
550def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500551 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000552 '-t', '--timeout',
553 type='float',
554 default=DEFAULT_SHARD_WAIT_TIME,
555 help='Timeout to wait for result, set to 0 for no timeout; default: '
556 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500557 parser.group_logging.add_option(
558 '--decorate', action='store_true', help='Decorate output')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000559
560
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500561@subcommand.usage('task_name')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000562def CMDcollect(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500563 """Retrieves results of a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000564
565 The result can be in multiple part if the execution was sharded. It can
566 potentially have retries.
567 """
568 add_collect_options(parser)
569 (options, args) = parser.parse_args(args)
570 if not args:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500571 parser.error('Must specify one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000572 elif len(args) > 1:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500573 parser.error('Must specify only one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000574
575 try:
576 return collect(options.swarming, args[0], options.timeout, options.decorate)
577 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000578 tools.report_error(e)
579 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000580
581
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500582@subcommand.usage('[hash|isolated]')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000583def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500584 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000585
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500586 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000587 """
588 add_trigger_options(parser)
589 add_collect_options(parser)
590 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500591 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000592
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500593 try:
594 result = trigger(
595 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500596 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500597 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500598 file_hash_or_isolated=args[0],
599 task_name=options.task_name,
600 shards=options.shards,
601 dimensions=options.dimensions,
602 env=dict(options.env),
603 working_dir=options.working_dir,
604 verbose=options.verbose,
605 profile=options.profile,
606 priority=options.priority)
607 except Failure as e:
608 tools.report_error(
609 'Failed to trigger %s(%s): %s' %
610 (options.task_name, args[0], e.args[0]))
611 return 1
612 if result:
613 tools.report_error('Failed to trigger the task.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000614 return result
615
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500616 try:
617 return collect(
618 options.swarming,
619 options.task_name,
620 options.timeout,
621 options.decorate)
622 except Failure as e:
623 tools.report_error(e)
624 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625
626
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500627@subcommand.usage("(hash|isolated)")
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500629 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000630
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500631 Accepts either the hash (sha1) of a .isolated file already uploaded or the
632 path to an .isolated file to archive, packages it if needed and sends a
633 Swarming manifest file to the Swarming server.
634
635 If an .isolated file is specified instead of an hash, it is first archived.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000636 """
637 add_trigger_options(parser)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500638 options, args = parser.parse_args(args)
639 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640
641 try:
642 return trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500643 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500644 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500645 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500646 file_hash_or_isolated=args[0],
647 task_name=options.task_name,
648 dimensions=options.dimensions,
649 shards=options.shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500650 env=dict(options.env),
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500651 working_dir=options.working_dir,
652 verbose=options.verbose,
653 profile=options.profile,
654 priority=options.priority)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000655 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000656 tools.report_error(e)
657 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000658
659
660class OptionParserSwarming(tools.OptionParserWithLogging):
661 def __init__(self, **kwargs):
662 tools.OptionParserWithLogging.__init__(
663 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500664 self.server_group = tools.optparse.OptionGroup(self, 'Server')
665 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000666 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -0500667 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000668 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500669 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800670 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000671
672 def parse_args(self, *args, **kwargs):
673 options, args = tools.OptionParserWithLogging.parse_args(
674 self, *args, **kwargs)
675 options.swarming = options.swarming.rstrip('/')
676 if not options.swarming:
677 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800678 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 return options, args
680
681
682def main(args):
683 dispatcher = subcommand.CommandDispatcher(__name__)
684 try:
685 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000686 except Exception as e:
687 tools.report_error(e)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000688 return 1
689
690
691if __name__ == '__main__':
692 fix_encoding.fix_encoding()
693 tools.disable_buffering()
694 colorama.init()
695 sys.exit(main(sys.argv[1:]))