blob: b8861412b7ac300cc36883c55621892c35b15b6a [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
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07008__version__ = '0.4.6'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040010import datetime
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -050011import getpass
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import hashlib
13import json
14import logging
15import os
Vadim Shtayurae3fbd102014-04-29 17:05:21 -070016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import shutil
maruel@chromium.org0437a732013-08-27 16:05:52 +000018import subprocess
19import sys
Vadim Shtayurab19319e2014-04-27 08:50:06 -070020import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000021import time
22import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000023
24from third_party import colorama
25from third_party.depot_tools import fix_encoding
26from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000027
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050028from utils import file_path
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040029from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000030from utils import net
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
maruel@chromium.org7b844a62013-09-17 13:04:59 +000036import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000037import run_isolated
38
39
40ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
41TOOLS_PATH = os.path.join(ROOT_DIR, 'tools')
42
43
maruel@chromium.org0437a732013-08-27 16:05:52 +000044# The default time to wait for a shard to finish running.
csharp@chromium.org24758492013-08-28 19:10:54 +000045DEFAULT_SHARD_WAIT_TIME = 80 * 60.
maruel@chromium.org0437a732013-08-27 16:05:52 +000046
Vadim Shtayura86a2cef2014-04-18 11:13:39 -070047# How often to print status updates to stdout in 'collect'.
48STATUS_UPDATE_INTERVAL = 15 * 60.
49
maruel@chromium.org0437a732013-08-27 16:05:52 +000050
51NO_OUTPUT_FOUND = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050052 'No output produced by the task, it may have failed to run.\n'
maruel@chromium.org0437a732013-08-27 16:05:52 +000053 '\n')
54
55
maruel@chromium.org0437a732013-08-27 16:05:52 +000056class Failure(Exception):
57 """Generic failure."""
58 pass
59
60
61class Manifest(object):
62 """Represents a Swarming task manifest.
63
64 Also includes code to zip code and upload itself.
65 """
66 def __init__(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050067 self, isolate_server, namespace, isolated_hash, task_name, shards, env,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -040068 dimensions, working_dir, deadline, verbose, profile, 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.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000073 isolated_hash - The manifest's sha-1 that the slave is going to fetch.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050074 task_name - The name to give the task request.
75 shards - The number of swarming shards to request.
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.
maruel@chromium.org0437a732013-08-27 16:05:52 +000078 working_dir - Relative working directory to start the script.
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -040079 deadline - maximum pending time before this task expires.
maruel@chromium.org0437a732013-08-27 16:05:52 +000080 verbose - if True, have the slave print more details.
81 profile - if True, have the slave print more timing data.
maruel@chromium.org7b844a62013-09-17 13:04:59 +000082 priority - int between 0 and 1000, lower the higher priority.
maruel@chromium.org0437a732013-08-27 16:05:52 +000083 """
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050084 self.isolate_server = isolate_server
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050085 self.namespace = namespace
86 # The reason is that swarm_bot doesn't understand compressed data yet. So
87 # the data to be downloaded by swarm_bot is in 'default', independent of
88 # what run_isolated.py is going to fetch.
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050089 self.storage = isolateserver.get_storage(isolate_server, 'default')
90
maruel@chromium.org814d23f2013-10-01 19:08:00 +000091 self.isolated_hash = isolated_hash
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000092 self.bundle = zip_package.ZipPackage(ROOT_DIR)
93
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050094 self._task_name = task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +000095 self._shards = shards
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050096 self._env = env.copy()
97 self._dimensions = dimensions.copy()
maruel@chromium.org0437a732013-08-27 16:05:52 +000098 self._working_dir = working_dir
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -040099 self._deadline = deadline
maruel@chromium.org0437a732013-08-27 16:05:52 +0000100
maruel@chromium.org0437a732013-08-27 16:05:52 +0000101 self.verbose = bool(verbose)
102 self.profile = bool(profile)
103 self.priority = priority
104
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000105 self._isolate_item = None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000106 self._tasks = []
maruel@chromium.org0437a732013-08-27 16:05:52 +0000107
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400108 def add_task(self, task_name, actions, time_out=2*60*60):
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500109 """Appends a new task as a TestObject to the swarming manifest file.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500110
111 Tasks cannot be added once the manifest was uploaded.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500112
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400113 By default, command will be killed after 2 hours of execution.
114
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500115 See TestObject in services/swarming/src/common/test_request_message.py for
116 the valid format.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500117 """
118 assert not self._isolate_item
maruel@chromium.org0437a732013-08-27 16:05:52 +0000119 self._tasks.append(
120 {
121 'action': actions,
122 'decorate_output': self.verbose,
123 'test_name': task_name,
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400124 'hard_time_out': time_out,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000125 })
126
maruel@chromium.org0437a732013-08-27 16:05:52 +0000127 def to_json(self):
128 """Exports the current configuration into a swarm-readable manifest file.
129
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500130 The actual serialization format is defined as a TestCase object as described
131 in services/swarming/src/common/test_request_message.py
132
maruel@chromium.org0437a732013-08-27 16:05:52 +0000133 This function doesn't mutate the object.
134 """
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500135 request = {
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500136 'cleanup': 'root',
maruel@chromium.org0437a732013-08-27 16:05:52 +0000137 'configurations': [
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500138 # Is a TestConfiguration.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000139 {
Marc-Antoine Ruel5d799192013-11-06 15:20:39 -0500140 'config_name': 'isolated',
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400141 'deadline_to_run': self._deadline,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500142 'dimensions': self._dimensions,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500143 'min_instances': self._shards,
144 'priority': self.priority,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000145 },
146 ],
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500147 'data': [],
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500148 'encoding': 'UTF-8',
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -0500149 'env_vars': self._env,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000150 'restart_on_failure': True,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500151 'test_case_name': self._task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500152 'tests': self._tasks,
153 'working_dir': self._working_dir,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000154 }
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000155 if self._isolate_item:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500156 request['data'].append(
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000157 [
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800158 self.storage.get_fetch_url(self._isolate_item),
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000159 'swarm_data.zip',
160 ])
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500161 return json.dumps(request, sort_keys=True, separators=(',',':'))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000162
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500163 @property
164 def isolate_item(self):
165 """Calling this property 'closes' the manifest and it can't be modified
166 afterward.
167 """
168 if self._isolate_item is None:
169 self._isolate_item = isolateserver.BufferItem(
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800170 self.bundle.zip_into_buffer(), high_priority=True)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500171 return self._isolate_item
172
173
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700174class TaskOutputCollector(object):
175 """Fetches task output from isolate server to local disk.
176
177 This object is shared among multiple threads running 'retrieve_results'
178 function, in particular they call 'process_shard_result' method in parallel.
179 """
180
181 def __init__(self, task_output_dir, task_name, shard_count):
182 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
183
184 Args:
185 task_output_dir: local directory to put fetched files to.
186 task_name: name of the swarming task results belong to.
187 shard_count: expected number of task shards.
188 """
189 self.task_output_dir = task_output_dir
190 self.task_name = task_name
191 self.shard_count = shard_count
192
193 self._lock = threading.Lock()
194 self._per_shard_results = {}
195 self._storage = None
196
197 if not os.path.isdir(self.task_output_dir):
198 os.makedirs(self.task_output_dir)
199
200 def process_shard_result(self, result):
201 """Stores results of a single task shard, fetches output files if necessary.
202
203 Called concurrently from multiple threads.
204 """
205 # We are going to put |shard_index| into a file path. Make sure it is int.
206 shard_index = result['config_instance_index']
207 if not isinstance(shard_index, int):
208 raise ValueError('Shard index should be an int: %r' % (shard_index,))
209
210 # Sanity check index is in expected range.
211 if shard_index < 0 or shard_index >= self.shard_count:
212 logging.warning(
213 'Shard index %d is outside of expected range: [0; %d]',
214 shard_index, self.shard_count - 1)
215 return
216
217 # Store result dict of that shard, ignore results we've already seen.
218 with self._lock:
219 if shard_index in self._per_shard_results:
220 logging.warning('Ignoring duplicate shard index %d', shard_index)
221 return
222 self._per_shard_results[shard_index] = result
223
224 # Fetch output files if necessary.
225 isolated_files_location = extract_output_files_location(result['output'])
226 if isolated_files_location:
227 isolate_server, namespace, isolated_hash = isolated_files_location
228 storage = self._get_storage(isolate_server, namespace)
229 if storage:
230 # Output files are supposed to be small and they are not reused across
231 # tasks. So use MemoryCache for them instead of on-disk cache. Make
232 # files writable, so that calling script can delete them.
233 isolateserver.fetch_isolated(
234 isolated_hash,
235 storage,
236 isolateserver.MemoryCache(file_mode_mask=0700),
237 os.path.join(self.task_output_dir, str(shard_index)),
238 False)
239
240 def finalize(self):
241 """Writes summary.json, shutdowns underlying Storage."""
242 with self._lock:
243 # Write an array of shard results with None for missing shards.
244 summary = {
245 'task_name': self.task_name,
246 'shards': [
247 self._per_shard_results.get(i) for i in xrange(self.shard_count)
248 ],
249 }
250 tools.write_json(
251 os.path.join(self.task_output_dir, 'summary.json'),
252 summary,
253 False)
254 if self._storage:
255 self._storage.close()
256 self._storage = None
257
258 def _get_storage(self, isolate_server, namespace):
259 """Returns isolateserver.Storage to use to fetch files."""
260 with self._lock:
261 if not self._storage:
262 self._storage = isolateserver.get_storage(isolate_server, namespace)
263 else:
264 # Shards must all use exact same isolate server and namespace.
265 if self._storage.location != isolate_server:
266 logging.error(
267 'Task shards are using multiple isolate servers: %s and %s',
268 self._storage.location, isolate_server)
269 return None
270 if self._storage.namespace != namespace:
271 logging.error(
272 'Task shards are using multiple namespaces: %s and %s',
273 self._storage.namespace, namespace)
274 return None
275 return self._storage
276
277
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500278def zip_and_upload(manifest):
279 """Zips up all the files necessary to run a manifest and uploads to Swarming
280 master.
281 """
282 try:
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700283 start_time = now()
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500284 with manifest.storage:
285 uploaded = manifest.storage.upload_items([manifest.isolate_item])
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700286 elapsed = now() - start_time
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500287 except (IOError, OSError) as exc:
288 tools.report_error('Failed to upload the zip file: %s' % exc)
289 return False
290
291 if manifest.isolate_item in uploaded:
292 logging.info('Upload complete, time elapsed: %f', elapsed)
293 else:
294 logging.info('Zip file already on server, time elapsed: %f', elapsed)
295 return True
296
maruel@chromium.org0437a732013-08-27 16:05:52 +0000297
298def now():
299 """Exists so it can be mocked easily."""
300 return time.time()
301
302
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500303def get_task_keys(swarm_base_url, task_name):
304 """Returns the Swarming task key for each shards of task_name."""
305 key_data = urllib.urlencode([('name', task_name)])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000306 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data)
307
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000308 for _ in net.retry_loop(max_attempts=net.URL_OPEN_MAX_ATTEMPTS):
309 result = net.url_read(url, retry_404=True)
310 if result is None:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000311 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500312 'Error: Unable to find any task with the name, %s, on swarming server'
313 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000314
maruel@chromium.org0437a732013-08-27 16:05:52 +0000315 # TODO(maruel): Compare exact string.
316 if 'No matching' in result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500317 logging.warning('Unable to find any task with the name, %s, on swarming '
318 'server' % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000319 continue
320 return json.loads(result)
321
322 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500323 'Error: Unable to find any task with the name, %s, on swarming server'
324 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000325
326
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700327def extract_output_files_location(task_log):
328 """Task log -> location of task output files to fetch.
329
330 TODO(vadimsh,maruel): Use side-channel to get this information.
331 See 'run_tha_test' in run_isolated.py for where the data is generated.
332
333 Returns:
334 Tuple (isolate server URL, namespace, isolated hash) on success.
335 None if information is missing or can not be parsed.
336 """
337 match = re.search(
338 r'\[run_isolated_out_hack\](.*)\[/run_isolated_out_hack\]',
339 task_log,
340 re.DOTALL)
341 if not match:
342 return None
343
344 def to_ascii(val):
345 if not isinstance(val, basestring):
346 raise ValueError()
347 return val.encode('ascii')
348
349 try:
350 data = json.loads(match.group(1))
351 if not isinstance(data, dict):
352 raise ValueError()
353 isolated_hash = to_ascii(data['hash'])
354 namespace = to_ascii(data['namespace'])
355 isolate_server = to_ascii(data['storage'])
356 if not file_path.is_url(isolate_server):
357 raise ValueError()
358 return (isolate_server, namespace, isolated_hash)
359 except (KeyError, ValueError):
360 logging.warning(
361 'Unexpected value of run_isolated_out_hack: %s', match.group(1))
362 return None
363
364
365def retrieve_results(
366 base_url, task_key, timeout, should_stop, output_collector):
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700367 """Retrieves results for a single task_key.
368
369 Returns a dict with results on success or None on failure or timeout.
370 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000371 assert isinstance(timeout, float), timeout
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500372 params = [('r', task_key)]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000373 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700374 started = now()
375 deadline = started + timeout if timeout else None
376 attempt = 0
377
378 while not should_stop.is_set():
379 attempt += 1
380
381 # Waiting for too long -> give up.
382 current_time = now()
383 if deadline and current_time >= deadline:
384 logging.error('retrieve_results(%s) timed out on attempt %d',
385 base_url, attempt)
386 return None
387
388 # Do not spin too fast. Spin faster at the beginning though.
389 # Start with 1 sec delay and for each 30 sec of waiting add another second
390 # of delay, until hitting 15 sec ceiling.
391 if attempt > 1:
392 max_delay = min(15, 1 + (current_time - started) / 30.0)
393 delay = min(max_delay, deadline - current_time) if deadline else max_delay
394 if delay > 0:
395 logging.debug('Waiting %.1f sec before retrying', delay)
396 should_stop.wait(delay)
397 if should_stop.is_set():
398 return None
399
400 # Disable internal retries in net.url_read, since we are doing retries
401 # ourselves. Do not use retry_404 so should_stop is polled more often.
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000402 response = net.url_read(result_url, retry_404=False, retry_50x=False)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700403
404 # Request failed. Try again.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000405 if response is None:
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700406 continue
407
408 # Got some response, ensure it is JSON dict, retry if not.
409 try:
410 result = json.loads(response) or {}
411 if not isinstance(result, dict):
412 raise ValueError()
413 except (ValueError, TypeError):
414 logging.warning(
415 'Received corrupted or invalid data for task_key %s, retrying: %r',
416 task_key, response)
417 continue
418
419 # Swarming server uses non-empty 'output' value as a flag that task has
420 # finished. How to wait for tasks that produce no output is a mystery.
421 if result.get('output'):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700422 # Record the result, try to fetch attached output files (if any).
423 if output_collector:
424 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
425 output_collector.process_shard_result(result)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700426 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000427
428
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700429def yield_results(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430 swarm_base_url, task_keys, timeout, max_threads,
431 print_status_updates, output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500432 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000433
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700434 Duplicate shards are ignored. Shards are yielded in order of completion.
435 Timed out shards are NOT yielded at all. Caller can compare number of yielded
436 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000437
438 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500439 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 +0000440 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500441
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442 output_collector is an optional instance of TaskOutputCollector that will be
443 used to fetch files produced by a task from isolate server to the local disk.
444
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500445 Yields:
446 (index, result). In particular, 'result' is defined as the
447 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000448 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000449 number_threads = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500450 min(max_threads, len(task_keys)) if max_threads else len(task_keys))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700451 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700452 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700453
maruel@chromium.org0437a732013-08-27 16:05:52 +0000454 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
455 try:
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700456 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500457 for task_key in task_keys:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000458 pool.add_task(
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700459 0, results_channel.wrap_task(retrieve_results),
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700460 swarm_base_url, task_key, timeout, should_stop, output_collector)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700461
462 # Wait for all of them to finish.
463 shards_remaining = range(len(task_keys))
464 active_task_count = len(task_keys)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700465 while active_task_count:
466 try:
467 result = results_channel.pull(timeout=STATUS_UPDATE_INTERVAL)
468 except threading_utils.TaskChannel.Timeout:
469 if print_status_updates:
470 print(
471 'Waiting for results from the following shards: %s' %
472 ', '.join(map(str, shards_remaining)))
473 sys.stdout.flush()
474 continue
475 except Exception:
476 logging.exception('Unexpected exception in retrieve_results')
477 result = None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700478
479 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700480 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000481 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500482 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000483 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700484
maruel@chromium.org0437a732013-08-27 16:05:52 +0000485 shard_index = result['config_instance_index']
486 if shard_index in shards_remaining:
487 shards_remaining.remove(shard_index)
488 yield shard_index, result
489 else:
490 logging.warning('Ignoring duplicate shard index %d', shard_index)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700491
maruel@chromium.org0437a732013-08-27 16:05:52 +0000492 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700493 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000494 should_stop.set()
495
496
497def chromium_setup(manifest):
498 """Sets up the commands to run.
499
500 Highly chromium specific.
501 """
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000502 # Add uncompressed zip here. It'll be compressed as part of the package sent
503 # to Swarming server.
504 run_test_name = 'run_isolated.zip'
505 manifest.bundle.add_buffer(run_test_name,
506 run_isolated.get_as_zip_package().zip_into_buffer(compress=False))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000507
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000508 cleanup_script_name = 'swarm_cleanup.py'
509 manifest.bundle.add_file(os.path.join(TOOLS_PATH, cleanup_script_name),
510 cleanup_script_name)
511
maruel@chromium.org0437a732013-08-27 16:05:52 +0000512 run_cmd = [
513 'python', run_test_name,
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000514 '--hash', manifest.isolated_hash,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500515 '--namespace', manifest.namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000516 ]
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500517 if file_path.is_url(manifest.isolate_server):
518 run_cmd.extend(('--isolate-server', manifest.isolate_server))
519 else:
520 run_cmd.extend(('--indir', manifest.isolate_server))
521
maruel@chromium.org0437a732013-08-27 16:05:52 +0000522 if manifest.verbose or manifest.profile:
523 # Have it print the profiling section.
524 run_cmd.append('--verbose')
525 manifest.add_task('Run Test', run_cmd)
526
527 # Clean up
528 manifest.add_task('Clean Up', ['python', cleanup_script_name])
529
530
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500531def googletest_setup(env, shards):
532 """Sets googletest specific environment variables."""
533 if shards > 1:
534 env = env.copy()
535 env['GTEST_SHARD_INDEX'] = '%(instance_index)s'
536 env['GTEST_TOTAL_SHARDS'] = '%(num_instances)s'
537 return env
538
539
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500540def archive(isolate_server, namespace, isolated, algo, verbose):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000541 """Archives a .isolated and all the dependencies on the CAC."""
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500542 logging.info('archive(%s, %s, %s)', isolate_server, namespace, isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000543 tempdir = None
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500544 if file_path.is_url(isolate_server):
545 command = 'archive'
546 flag = '--isolate-server'
547 else:
548 command = 'hashtable'
549 flag = '--outdir'
550
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500551 print('Archiving: %s' % isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000552 try:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000553 cmd = [
554 sys.executable,
555 os.path.join(ROOT_DIR, 'isolate.py'),
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500556 command,
557 flag, isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500558 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000559 '--isolated', isolated,
560 ]
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000561 cmd.extend(['--verbose'] * verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000562 logging.info(' '.join(cmd))
563 if subprocess.call(cmd, verbose):
564 return
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000565 return isolateserver.hash_file(isolated, algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000566 finally:
567 if tempdir:
568 shutil.rmtree(tempdir)
569
570
571def process_manifest(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500572 swarming, isolate_server, namespace, isolated_hash, task_name, shards,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400573 dimensions, env, working_dir, deadline, verbose, profile, priority):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500574 """Processes the manifest file and send off the swarming task request."""
maruel@chromium.org0437a732013-08-27 16:05:52 +0000575 try:
576 manifest = Manifest(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500577 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500578 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500579 isolated_hash=isolated_hash,
580 task_name=task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500581 shards=shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500582 dimensions=dimensions,
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -0500583 env=env,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500584 working_dir=working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400585 deadline=deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500586 verbose=verbose,
587 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800588 priority=priority)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000589 except ValueError as e:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500590 tools.report_error('Unable to process %s: %s' % (task_name, e))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000591 return 1
592
593 chromium_setup(manifest)
594
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500595 logging.info('Zipping up files...')
596 if not zip_and_upload(manifest):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000597 return 1
598
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500599 logging.info('Server: %s', swarming)
600 logging.info('Task name: %s', task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500601 trigger_url = swarming + '/test'
maruel@chromium.org0437a732013-08-27 16:05:52 +0000602 manifest_text = manifest.to_json()
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500603 result = net.url_read(trigger_url, data={'request': manifest_text})
maruel@chromium.org0437a732013-08-27 16:05:52 +0000604 if not result:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000605 tools.report_error(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500606 'Failed to trigger task %s\n%s' % (task_name, trigger_url))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000607 return 1
608 try:
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000609 json.loads(result)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000610 except (ValueError, TypeError) as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000611 msg = '\n'.join((
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500612 'Failed to trigger task %s' % task_name,
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000613 'Manifest: %s' % manifest_text,
614 'Bad response: %s' % result,
615 str(e)))
616 tools.report_error(msg)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617 return 1
618 return 0
619
620
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500621def isolated_to_hash(isolate_server, namespace, arg, algo, verbose):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500622 """Archives a .isolated file if needed.
623
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500624 Returns the file hash to trigger and a bool specifying if it was a file (True)
625 or a hash (False).
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500626 """
627 if arg.endswith('.isolated'):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500628 file_hash = archive(isolate_server, namespace, arg, algo, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500629 if not file_hash:
630 tools.report_error('Archival failure %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500631 return None, True
632 return file_hash, True
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500633 elif isolateserver.is_valid_hash(arg, algo):
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500634 return arg, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500635 else:
636 tools.report_error('Invalid hash %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500637 return None, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500638
639
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640def trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500641 swarming,
642 isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500643 namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500644 file_hash_or_isolated,
645 task_name,
646 shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500647 dimensions,
648 env,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500649 working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400650 deadline,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000651 verbose,
652 profile,
653 priority):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500654 """Sends off the hash swarming task requests."""
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500655 file_hash, is_file = isolated_to_hash(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500656 isolate_server, namespace, file_hash_or_isolated, hashlib.sha1, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500657 if not file_hash:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500658 return 1, ''
659 if not task_name:
660 # If a file name was passed, use its base name of the isolated hash.
661 # Otherwise, use user name as an approximation of a task name.
662 if is_file:
663 key = os.path.splitext(os.path.basename(file_hash_or_isolated))[0]
664 else:
665 key = getpass.getuser()
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700666 task_name = '%s/%s/%s/%d' % (
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500667 key,
668 '_'.join('%s=%s' % (k, v) for k, v in sorted(dimensions.iteritems())),
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700669 file_hash,
670 now() * 1000)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500671
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500672 env = googletest_setup(env, shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500673 # TODO(maruel): It should first create a request manifest object, then pass
674 # it to a function to zip, archive and trigger.
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500675 result = process_manifest(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500676 swarming=swarming,
677 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500678 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500679 isolated_hash=file_hash,
680 task_name=task_name,
681 shards=shards,
682 dimensions=dimensions,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400683 deadline=deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500684 env=env,
685 working_dir=working_dir,
686 verbose=verbose,
687 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800688 priority=priority)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500689 return result, task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690
691
692def decorate_shard_output(result, shard_exit_code):
693 """Returns wrapped output for swarming task shard."""
694 tag = 'index %s (machine tag: %s, id: %s)' % (
695 result['config_instance_index'],
696 result['machine_id'],
697 result.get('machine_tag', 'unknown'))
698 return (
699 '\n'
700 '================================================================\n'
701 'Begin output from shard %s\n'
702 '================================================================\n'
703 '\n'
704 '%s'
705 '================================================================\n'
706 'End output from shard %s. Return %d\n'
707 '================================================================\n'
708 ) % (tag, result['output'] or NO_OUTPUT_FOUND, tag, shard_exit_code)
709
710
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700711def collect(
712 url, task_name, timeout, decorate, print_status_updates, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500713 """Retrieves results of a Swarming task."""
714 logging.info('Collecting %s', task_name)
715 task_keys = get_task_keys(url, task_name)
716 if not task_keys:
717 raise Failure('No task keys to get results with.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000718
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700719 # Collect output files only if explicitly asked with --task-output-dir option.
720 if task_output_dir:
721 output_collector = TaskOutputCollector(
722 task_output_dir, task_name, len(task_keys))
723 else:
724 output_collector = None
725
maruel@chromium.org9c1c7b52013-08-28 19:04:36 +0000726 exit_code = None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700727 seen_shards = set()
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700728
729 try:
730 for index, output in yield_results(
731 url, task_keys, timeout, None, print_status_updates, output_collector):
732 seen_shards.add(index)
733 shard_exit_codes = (output['exit_codes'] or '1').split(',')
734 shard_exit_code = max(int(i) for i in shard_exit_codes)
735 if decorate:
736 print decorate_shard_output(output, shard_exit_code)
737 else:
738 print(
739 '%s/%s: %s' % (
740 output['machine_id'],
741 output['machine_tag'],
742 output['exit_codes']))
743 print(''.join(' %s\n' % l for l in output['output'].splitlines()))
744 exit_code = exit_code or shard_exit_code
745 finally:
746 if output_collector:
747 output_collector.finalize()
748
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700749 if len(seen_shards) != len(task_keys):
750 missing_shards = [x for x in range(len(task_keys)) if x not in seen_shards]
751 print >> sys.stderr, ('Results from some shards are missing: %s' %
752 ', '.join(map(str, missing_shards)))
753 exit_code = exit_code or 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700754
maruel@chromium.org9c1c7b52013-08-28 19:04:36 +0000755 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000756
757
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400758def add_filter_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500759 parser.filter_group = tools.optparse.OptionGroup(parser, 'Filtering slaves')
760 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500761 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500762 dest='dimensions', metavar='FOO bar',
763 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500764 parser.add_option_group(parser.filter_group)
765
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400766
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400767def process_filter_options(parser, options):
768 options.dimensions = dict(options.dimensions)
769 if not options.dimensions:
770 parser.error('Please at least specify one --dimension')
771
772
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400773def add_trigger_options(parser):
774 """Adds all options to trigger a task on Swarming."""
775 isolateserver.add_isolate_server_options(parser, True)
776 add_filter_options(parser)
777
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500778 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
779 parser.task_group.add_option(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500780 '-w', '--working-dir', default='swarm_tests',
781 help='Working directory on the swarming slave side. default: %default.')
782 parser.task_group.add_option(
783 '--working_dir', help=tools.optparse.SUPPRESS_HELP)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500784 parser.task_group.add_option(
785 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
786 help='environment variables to set')
787 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500788 '--priority', type='int', default=100,
789 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500790 parser.task_group.add_option(
791 '--shards', type='int', default=1, help='number of shards to use')
792 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500793 '-T', '--task-name',
794 help='Display name of the task. It uniquely identifies the task. '
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700795 'Defaults to <base_name>/<dimensions>/<isolated hash>/<timestamp> '
796 'if an isolated file is provided, if a hash is provided, it '
797 'defaults to <user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400798 parser.task_group.add_option(
799 '--deadline', type='int', default=6*60*60,
800 help='Seconds to allow the task to be pending for a bot to run before '
801 'this task request expires.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500802 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500803 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500804 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000805 '--profile', action='store_true',
806 default=bool(os.environ.get('ISOLATE_DEBUG')),
807 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000808
809
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500810def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500811 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500812 if len(args) != 1:
813 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400814 process_filter_options(parser, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000815
816
817def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500818 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000819 '-t', '--timeout',
820 type='float',
821 default=DEFAULT_SHARD_WAIT_TIME,
822 help='Timeout to wait for result, set to 0 for no timeout; default: '
823 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500824 parser.group_logging.add_option(
825 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700826 parser.group_logging.add_option(
827 '--print-status-updates', action='store_true',
828 help='Print periodic status updates')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700829 parser.task_output_group = tools.optparse.OptionGroup(parser, 'Task output')
830 parser.task_output_group.add_option(
831 '--task-output-dir',
832 help='Directory to put task results into. When the task finishes, this '
833 'directory contains <task-output-dir>/summary.json file with '
834 'a summary of task results across all shards, and per-shard '
835 'directory with output files produced by a shard: '
836 '<task-output-dir>/<zero-based-shard-index>/')
837 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000838
839
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500840@subcommand.usage('task_name')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000841def CMDcollect(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500842 """Retrieves results of a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000843
844 The result can be in multiple part if the execution was sharded. It can
845 potentially have retries.
846 """
847 add_collect_options(parser)
848 (options, args) = parser.parse_args(args)
849 if not args:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500850 parser.error('Must specify one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000851 elif len(args) > 1:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500852 parser.error('Must specify only one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000853
854 try:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700855 return collect(
856 options.swarming,
857 args[0],
858 options.timeout,
859 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700860 options.print_status_updates,
861 options.task_output_dir)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000862 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000863 tools.report_error(e)
864 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000865
866
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400867def CMDquery(parser, args):
868 """Returns information about the bots connected to the Swarming server."""
869 add_filter_options(parser)
870 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400871 '--dead-only', action='store_true',
872 help='Only print dead bots, useful to reap them and reimage broken bots')
873 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400874 '-k', '--keep-dead', action='store_true',
875 help='Do not filter out dead bots')
876 parser.filter_group.add_option(
877 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400878 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400879 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400880
881 if options.keep_dead and options.dead_only:
882 parser.error('Use only one of --keep-dead and --dead-only')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400883 service = net.get_http_service(options.swarming)
884 data = service.json_request('GET', '/swarming/api/v1/bots')
885 if data is None:
886 print >> sys.stderr, 'Failed to access %s' % options.swarming
887 return 1
888 timeout = datetime.timedelta(seconds=data['machine_death_timeout'])
889 utcnow = datetime.datetime.utcnow()
890 for machine in natsort.natsorted(data['machines'], key=lambda x: x['tag']):
891 last_seen = datetime.datetime.strptime(
892 machine['last_seen'], '%Y-%m-%d %H:%M:%S')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400893 is_dead = utcnow - last_seen > timeout
894 if options.dead_only:
895 if not is_dead:
896 continue
897 elif not options.keep_dead and is_dead:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400898 continue
899
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400900 # If the user requested to filter on dimensions, ensure the bot has all the
901 # dimensions requested.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400902 dimensions = machine['dimensions']
903 for key, value in options.dimensions:
904 if key not in dimensions:
905 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400906 # A bot can have multiple value for a key, for example,
907 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
908 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909 if isinstance(dimensions[key], list):
910 if value not in dimensions[key]:
911 break
912 else:
913 if value != dimensions[key]:
914 break
915 else:
916 print machine['tag']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400917 if not options.bare:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400918 print ' %s' % dimensions
919 return 0
920
921
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500922@subcommand.usage('[hash|isolated]')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000923def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500924 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000925
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500926 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000927 """
928 add_trigger_options(parser)
929 add_collect_options(parser)
930 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500931 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000932
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500933 try:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500934 result, task_name = trigger(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500935 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500936 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500937 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500938 file_hash_or_isolated=args[0],
939 task_name=options.task_name,
940 shards=options.shards,
941 dimensions=options.dimensions,
942 env=dict(options.env),
943 working_dir=options.working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400944 deadline=options.deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500945 verbose=options.verbose,
946 profile=options.profile,
947 priority=options.priority)
948 except Failure as e:
949 tools.report_error(
950 'Failed to trigger %s(%s): %s' %
951 (options.task_name, args[0], e.args[0]))
952 return 1
953 if result:
954 tools.report_error('Failed to trigger the task.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000955 return result
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500956 if task_name != options.task_name:
957 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500958 try:
959 return collect(
960 options.swarming,
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500961 task_name,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500962 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700963 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700964 options.print_status_updates,
965 options.task_output_dir)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500966 except Failure as e:
967 tools.report_error(e)
968 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000969
970
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500971@subcommand.usage("(hash|isolated)")
maruel@chromium.org0437a732013-08-27 16:05:52 +0000972def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500973 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000974
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500975 Accepts either the hash (sha1) of a .isolated file already uploaded or the
976 path to an .isolated file to archive, packages it if needed and sends a
977 Swarming manifest file to the Swarming server.
978
979 If an .isolated file is specified instead of an hash, it is first archived.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000980 """
981 add_trigger_options(parser)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500982 options, args = parser.parse_args(args)
983 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000984
985 try:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500986 result, task_name = trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500987 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500988 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500989 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500990 file_hash_or_isolated=args[0],
991 task_name=options.task_name,
992 dimensions=options.dimensions,
993 shards=options.shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500994 env=dict(options.env),
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500995 working_dir=options.working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400996 deadline=options.deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500997 verbose=options.verbose,
998 profile=options.profile,
999 priority=options.priority)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001000 if task_name != options.task_name and not result:
1001 print('Triggered task: %s' % task_name)
1002 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +00001003 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001004 tools.report_error(e)
1005 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001006
1007
1008class OptionParserSwarming(tools.OptionParserWithLogging):
1009 def __init__(self, **kwargs):
1010 tools.OptionParserWithLogging.__init__(
1011 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001012 self.server_group = tools.optparse.OptionGroup(self, 'Server')
1013 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001014 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001015 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001016 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001017 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001018 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001019
1020 def parse_args(self, *args, **kwargs):
1021 options, args = tools.OptionParserWithLogging.parse_args(
1022 self, *args, **kwargs)
1023 options.swarming = options.swarming.rstrip('/')
1024 if not options.swarming:
1025 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001026 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001027 return options, args
1028
1029
1030def main(args):
1031 dispatcher = subcommand.CommandDispatcher(__name__)
1032 try:
1033 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001034 except Exception as e:
1035 tools.report_error(e)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001036 return 1
1037
1038
1039if __name__ == '__main__':
1040 fix_encoding.fix_encoding()
1041 tools.disable_buffering()
1042 colorama.init()
1043 sys.exit(main(sys.argv[1:]))