blob: cbb5e2395b04493ec69e97c0226235e73aec3e6c [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
767def add_trigger_options(parser):
768 """Adds all options to trigger a task on Swarming."""
769 isolateserver.add_isolate_server_options(parser, True)
770 add_filter_options(parser)
771
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500772 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
773 parser.task_group.add_option(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500774 '-w', '--working-dir', default='swarm_tests',
775 help='Working directory on the swarming slave side. default: %default.')
776 parser.task_group.add_option(
777 '--working_dir', help=tools.optparse.SUPPRESS_HELP)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500778 parser.task_group.add_option(
779 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
780 help='environment variables to set')
781 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500782 '--priority', type='int', default=100,
783 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500784 parser.task_group.add_option(
785 '--shards', type='int', default=1, help='number of shards to use')
786 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500787 '-T', '--task-name',
788 help='Display name of the task. It uniquely identifies the task. '
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700789 'Defaults to <base_name>/<dimensions>/<isolated hash>/<timestamp> '
790 'if an isolated file is provided, if a hash is provided, it '
791 'defaults to <user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400792 parser.task_group.add_option(
793 '--deadline', type='int', default=6*60*60,
794 help='Seconds to allow the task to be pending for a bot to run before '
795 'this task request expires.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500796 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500797 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500798 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000799 '--profile', action='store_true',
800 default=bool(os.environ.get('ISOLATE_DEBUG')),
801 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000802
803
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500804def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500805 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500806 if len(args) != 1:
807 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500808 options.dimensions = dict(options.dimensions)
Marc-Antoine Ruel2d1bee82014-03-12 14:10:25 -0400809 if not options.dimensions:
810 parser.error('Please at least specify one --dimension')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000811
812
813def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500814 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000815 '-t', '--timeout',
816 type='float',
817 default=DEFAULT_SHARD_WAIT_TIME,
818 help='Timeout to wait for result, set to 0 for no timeout; default: '
819 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500820 parser.group_logging.add_option(
821 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700822 parser.group_logging.add_option(
823 '--print-status-updates', action='store_true',
824 help='Print periodic status updates')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700825 parser.task_output_group = tools.optparse.OptionGroup(parser, 'Task output')
826 parser.task_output_group.add_option(
827 '--task-output-dir',
828 help='Directory to put task results into. When the task finishes, this '
829 'directory contains <task-output-dir>/summary.json file with '
830 'a summary of task results across all shards, and per-shard '
831 'directory with output files produced by a shard: '
832 '<task-output-dir>/<zero-based-shard-index>/')
833 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000834
835
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500836@subcommand.usage('task_name')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000837def CMDcollect(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500838 """Retrieves results of a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000839
840 The result can be in multiple part if the execution was sharded. It can
841 potentially have retries.
842 """
843 add_collect_options(parser)
844 (options, args) = parser.parse_args(args)
845 if not args:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500846 parser.error('Must specify one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000847 elif len(args) > 1:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500848 parser.error('Must specify only one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000849
850 try:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700851 return collect(
852 options.swarming,
853 args[0],
854 options.timeout,
855 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700856 options.print_status_updates,
857 options.task_output_dir)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000858 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000859 tools.report_error(e)
860 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000861
862
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400863def CMDquery(parser, args):
864 """Returns information about the bots connected to the Swarming server."""
865 add_filter_options(parser)
866 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400867 '--dead-only', action='store_true',
868 help='Only print dead bots, useful to reap them and reimage broken bots')
869 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400870 '-k', '--keep-dead', action='store_true',
871 help='Do not filter out dead bots')
872 parser.filter_group.add_option(
873 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400874 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400875 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400876
877 if options.keep_dead and options.dead_only:
878 parser.error('Use only one of --keep-dead and --dead-only')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400879 service = net.get_http_service(options.swarming)
880 data = service.json_request('GET', '/swarming/api/v1/bots')
881 if data is None:
882 print >> sys.stderr, 'Failed to access %s' % options.swarming
883 return 1
884 timeout = datetime.timedelta(seconds=data['machine_death_timeout'])
885 utcnow = datetime.datetime.utcnow()
886 for machine in natsort.natsorted(data['machines'], key=lambda x: x['tag']):
887 last_seen = datetime.datetime.strptime(
888 machine['last_seen'], '%Y-%m-%d %H:%M:%S')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400889 is_dead = utcnow - last_seen > timeout
890 if options.dead_only:
891 if not is_dead:
892 continue
893 elif not options.keep_dead and is_dead:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400894 continue
895
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400896 # If the user requested to filter on dimensions, ensure the bot has all the
897 # dimensions requested.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400898 dimensions = machine['dimensions']
899 for key, value in options.dimensions:
900 if key not in dimensions:
901 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400902 # A bot can have multiple value for a key, for example,
903 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
904 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400905 if isinstance(dimensions[key], list):
906 if value not in dimensions[key]:
907 break
908 else:
909 if value != dimensions[key]:
910 break
911 else:
912 print machine['tag']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400913 if not options.bare:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400914 print ' %s' % dimensions
915 return 0
916
917
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500918@subcommand.usage('[hash|isolated]')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000919def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500920 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000921
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500922 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000923 """
924 add_trigger_options(parser)
925 add_collect_options(parser)
926 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500927 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000928
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500929 try:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500930 result, task_name = trigger(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500931 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500932 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500933 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500934 file_hash_or_isolated=args[0],
935 task_name=options.task_name,
936 shards=options.shards,
937 dimensions=options.dimensions,
938 env=dict(options.env),
939 working_dir=options.working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400940 deadline=options.deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500941 verbose=options.verbose,
942 profile=options.profile,
943 priority=options.priority)
944 except Failure as e:
945 tools.report_error(
946 'Failed to trigger %s(%s): %s' %
947 (options.task_name, args[0], e.args[0]))
948 return 1
949 if result:
950 tools.report_error('Failed to trigger the task.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000951 return result
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500952 if task_name != options.task_name:
953 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500954 try:
955 return collect(
956 options.swarming,
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500957 task_name,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500958 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700959 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700960 options.print_status_updates,
961 options.task_output_dir)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500962 except Failure as e:
963 tools.report_error(e)
964 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000965
966
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500967@subcommand.usage("(hash|isolated)")
maruel@chromium.org0437a732013-08-27 16:05:52 +0000968def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500969 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000970
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500971 Accepts either the hash (sha1) of a .isolated file already uploaded or the
972 path to an .isolated file to archive, packages it if needed and sends a
973 Swarming manifest file to the Swarming server.
974
975 If an .isolated file is specified instead of an hash, it is first archived.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000976 """
977 add_trigger_options(parser)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500978 options, args = parser.parse_args(args)
979 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000980
981 try:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500982 result, task_name = trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500983 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500984 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500985 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500986 file_hash_or_isolated=args[0],
987 task_name=options.task_name,
988 dimensions=options.dimensions,
989 shards=options.shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500990 env=dict(options.env),
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500991 working_dir=options.working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400992 deadline=options.deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500993 verbose=options.verbose,
994 profile=options.profile,
995 priority=options.priority)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500996 if task_name != options.task_name and not result:
997 print('Triggered task: %s' % task_name)
998 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000999 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001000 tools.report_error(e)
1001 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001002
1003
1004class OptionParserSwarming(tools.OptionParserWithLogging):
1005 def __init__(self, **kwargs):
1006 tools.OptionParserWithLogging.__init__(
1007 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001008 self.server_group = tools.optparse.OptionGroup(self, 'Server')
1009 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001010 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001011 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001012 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001013 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001014 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001015
1016 def parse_args(self, *args, **kwargs):
1017 options, args = tools.OptionParserWithLogging.parse_args(
1018 self, *args, **kwargs)
1019 options.swarming = options.swarming.rstrip('/')
1020 if not options.swarming:
1021 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001022 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001023 return options, args
1024
1025
1026def main(args):
1027 dispatcher = subcommand.CommandDispatcher(__name__)
1028 try:
1029 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001030 except Exception as e:
1031 tools.report_error(e)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001032 return 1
1033
1034
1035if __name__ == '__main__':
1036 fix_encoding.fix_encoding()
1037 tools.disable_buffering()
1038 colorama.init()
1039 sys.exit(main(sys.argv[1:]))