blob: 7fbdb849d736ecf72420602558f301a2aaf63ea7 [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 Shtayuraae8085b2014-05-02 17:13:10 -07008__version__ = '0.4.7'
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__(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070067 self, isolate_server, namespace, isolated_hash, task_name, extra_args,
68 shards, env, dimensions, working_dir, deadline, verbose, profile,
69 priority):
maruel@chromium.org0437a732013-08-27 16:05:52 +000070 """Populates a manifest object.
71 Args:
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050072 isolate_server - isolate server url.
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050073 namespace - isolate server namespace to use.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070074 isolated_hash - the manifest's sha-1 that the slave is going to fetch.
75 task_name - the name to give the task request.
76 extra_args - additional arguments to pass to isolated command.
77 shards - the number of swarming shards to request.
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -050078 env - environment variables to set.
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050079 dimensions - dimensions to filter the task on.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070080 working_dir - relative working directory to start the script.
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -040081 deadline - maximum pending time before this task expires.
maruel@chromium.org0437a732013-08-27 16:05:52 +000082 verbose - if True, have the slave print more details.
83 profile - if True, have the slave print more timing data.
maruel@chromium.org7b844a62013-09-17 13:04:59 +000084 priority - int between 0 and 1000, lower the higher priority.
maruel@chromium.org0437a732013-08-27 16:05:52 +000085 """
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050086 self.isolate_server = isolate_server
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050087 self.namespace = namespace
88 # The reason is that swarm_bot doesn't understand compressed data yet. So
89 # the data to be downloaded by swarm_bot is in 'default', independent of
90 # what run_isolated.py is going to fetch.
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050091 self.storage = isolateserver.get_storage(isolate_server, 'default')
92
maruel@chromium.org814d23f2013-10-01 19:08:00 +000093 self.isolated_hash = isolated_hash
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070094 self.extra_args = tuple(extra_args or [])
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000095 self.bundle = zip_package.ZipPackage(ROOT_DIR)
96
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050097 self._task_name = task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +000098 self._shards = shards
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050099 self._env = env.copy()
100 self._dimensions = dimensions.copy()
maruel@chromium.org0437a732013-08-27 16:05:52 +0000101 self._working_dir = working_dir
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400102 self._deadline = deadline
maruel@chromium.org0437a732013-08-27 16:05:52 +0000103
maruel@chromium.org0437a732013-08-27 16:05:52 +0000104 self.verbose = bool(verbose)
105 self.profile = bool(profile)
106 self.priority = priority
107
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000108 self._isolate_item = None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000109 self._tasks = []
maruel@chromium.org0437a732013-08-27 16:05:52 +0000110
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400111 def add_task(self, task_name, actions, time_out=2*60*60):
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500112 """Appends a new task as a TestObject to the swarming manifest file.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500113
114 Tasks cannot be added once the manifest was uploaded.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500115
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400116 By default, command will be killed after 2 hours of execution.
117
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500118 See TestObject in services/swarming/src/common/test_request_message.py for
119 the valid format.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500120 """
121 assert not self._isolate_item
maruel@chromium.org0437a732013-08-27 16:05:52 +0000122 self._tasks.append(
123 {
124 'action': actions,
125 'decorate_output': self.verbose,
126 'test_name': task_name,
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400127 'hard_time_out': time_out,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000128 })
129
maruel@chromium.org0437a732013-08-27 16:05:52 +0000130 def to_json(self):
131 """Exports the current configuration into a swarm-readable manifest file.
132
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500133 The actual serialization format is defined as a TestCase object as described
134 in services/swarming/src/common/test_request_message.py
135
maruel@chromium.org0437a732013-08-27 16:05:52 +0000136 This function doesn't mutate the object.
137 """
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500138 request = {
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500139 'cleanup': 'root',
maruel@chromium.org0437a732013-08-27 16:05:52 +0000140 'configurations': [
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500141 # Is a TestConfiguration.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000142 {
Marc-Antoine Ruel5d799192013-11-06 15:20:39 -0500143 'config_name': 'isolated',
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400144 'deadline_to_run': self._deadline,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500145 'dimensions': self._dimensions,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500146 'min_instances': self._shards,
147 'priority': self.priority,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000148 },
149 ],
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500150 'data': [],
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500151 'encoding': 'UTF-8',
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -0500152 'env_vars': self._env,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000153 'restart_on_failure': True,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500154 'test_case_name': self._task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500155 'tests': self._tasks,
156 'working_dir': self._working_dir,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000157 }
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000158 if self._isolate_item:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500159 request['data'].append(
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000160 [
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800161 self.storage.get_fetch_url(self._isolate_item),
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000162 'swarm_data.zip',
163 ])
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500164 return json.dumps(request, sort_keys=True, separators=(',',':'))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000165
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500166 @property
167 def isolate_item(self):
168 """Calling this property 'closes' the manifest and it can't be modified
169 afterward.
170 """
171 if self._isolate_item is None:
172 self._isolate_item = isolateserver.BufferItem(
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800173 self.bundle.zip_into_buffer(), high_priority=True)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500174 return self._isolate_item
175
176
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700177class TaskOutputCollector(object):
178 """Fetches task output from isolate server to local disk.
179
180 This object is shared among multiple threads running 'retrieve_results'
181 function, in particular they call 'process_shard_result' method in parallel.
182 """
183
184 def __init__(self, task_output_dir, task_name, shard_count):
185 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
186
187 Args:
188 task_output_dir: local directory to put fetched files to.
189 task_name: name of the swarming task results belong to.
190 shard_count: expected number of task shards.
191 """
192 self.task_output_dir = task_output_dir
193 self.task_name = task_name
194 self.shard_count = shard_count
195
196 self._lock = threading.Lock()
197 self._per_shard_results = {}
198 self._storage = None
199
200 if not os.path.isdir(self.task_output_dir):
201 os.makedirs(self.task_output_dir)
202
203 def process_shard_result(self, result):
204 """Stores results of a single task shard, fetches output files if necessary.
205
206 Called concurrently from multiple threads.
207 """
208 # We are going to put |shard_index| into a file path. Make sure it is int.
209 shard_index = result['config_instance_index']
210 if not isinstance(shard_index, int):
211 raise ValueError('Shard index should be an int: %r' % (shard_index,))
212
213 # Sanity check index is in expected range.
214 if shard_index < 0 or shard_index >= self.shard_count:
215 logging.warning(
216 'Shard index %d is outside of expected range: [0; %d]',
217 shard_index, self.shard_count - 1)
218 return
219
220 # Store result dict of that shard, ignore results we've already seen.
221 with self._lock:
222 if shard_index in self._per_shard_results:
223 logging.warning('Ignoring duplicate shard index %d', shard_index)
224 return
225 self._per_shard_results[shard_index] = result
226
227 # Fetch output files if necessary.
228 isolated_files_location = extract_output_files_location(result['output'])
229 if isolated_files_location:
230 isolate_server, namespace, isolated_hash = isolated_files_location
231 storage = self._get_storage(isolate_server, namespace)
232 if storage:
233 # Output files are supposed to be small and they are not reused across
234 # tasks. So use MemoryCache for them instead of on-disk cache. Make
235 # files writable, so that calling script can delete them.
236 isolateserver.fetch_isolated(
237 isolated_hash,
238 storage,
239 isolateserver.MemoryCache(file_mode_mask=0700),
240 os.path.join(self.task_output_dir, str(shard_index)),
241 False)
242
243 def finalize(self):
244 """Writes summary.json, shutdowns underlying Storage."""
245 with self._lock:
246 # Write an array of shard results with None for missing shards.
247 summary = {
248 'task_name': self.task_name,
249 'shards': [
250 self._per_shard_results.get(i) for i in xrange(self.shard_count)
251 ],
252 }
253 tools.write_json(
254 os.path.join(self.task_output_dir, 'summary.json'),
255 summary,
256 False)
257 if self._storage:
258 self._storage.close()
259 self._storage = None
260
261 def _get_storage(self, isolate_server, namespace):
262 """Returns isolateserver.Storage to use to fetch files."""
263 with self._lock:
264 if not self._storage:
265 self._storage = isolateserver.get_storage(isolate_server, namespace)
266 else:
267 # Shards must all use exact same isolate server and namespace.
268 if self._storage.location != isolate_server:
269 logging.error(
270 'Task shards are using multiple isolate servers: %s and %s',
271 self._storage.location, isolate_server)
272 return None
273 if self._storage.namespace != namespace:
274 logging.error(
275 'Task shards are using multiple namespaces: %s and %s',
276 self._storage.namespace, namespace)
277 return None
278 return self._storage
279
280
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500281def zip_and_upload(manifest):
282 """Zips up all the files necessary to run a manifest and uploads to Swarming
283 master.
284 """
285 try:
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700286 start_time = now()
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500287 with manifest.storage:
288 uploaded = manifest.storage.upload_items([manifest.isolate_item])
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700289 elapsed = now() - start_time
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500290 except (IOError, OSError) as exc:
291 tools.report_error('Failed to upload the zip file: %s' % exc)
292 return False
293
294 if manifest.isolate_item in uploaded:
295 logging.info('Upload complete, time elapsed: %f', elapsed)
296 else:
297 logging.info('Zip file already on server, time elapsed: %f', elapsed)
298 return True
299
maruel@chromium.org0437a732013-08-27 16:05:52 +0000300
301def now():
302 """Exists so it can be mocked easily."""
303 return time.time()
304
305
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500306def get_task_keys(swarm_base_url, task_name):
307 """Returns the Swarming task key for each shards of task_name."""
308 key_data = urllib.urlencode([('name', task_name)])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000309 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data)
310
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000311 for _ in net.retry_loop(max_attempts=net.URL_OPEN_MAX_ATTEMPTS):
312 result = net.url_read(url, retry_404=True)
313 if result is None:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000314 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500315 'Error: Unable to find any task with the name, %s, on swarming server'
316 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000317
maruel@chromium.org0437a732013-08-27 16:05:52 +0000318 # TODO(maruel): Compare exact string.
319 if 'No matching' in result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500320 logging.warning('Unable to find any task with the name, %s, on swarming '
321 'server' % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000322 continue
323 return json.loads(result)
324
325 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500326 'Error: Unable to find any task with the name, %s, on swarming server'
327 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000328
329
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700330def extract_output_files_location(task_log):
331 """Task log -> location of task output files to fetch.
332
333 TODO(vadimsh,maruel): Use side-channel to get this information.
334 See 'run_tha_test' in run_isolated.py for where the data is generated.
335
336 Returns:
337 Tuple (isolate server URL, namespace, isolated hash) on success.
338 None if information is missing or can not be parsed.
339 """
340 match = re.search(
341 r'\[run_isolated_out_hack\](.*)\[/run_isolated_out_hack\]',
342 task_log,
343 re.DOTALL)
344 if not match:
345 return None
346
347 def to_ascii(val):
348 if not isinstance(val, basestring):
349 raise ValueError()
350 return val.encode('ascii')
351
352 try:
353 data = json.loads(match.group(1))
354 if not isinstance(data, dict):
355 raise ValueError()
356 isolated_hash = to_ascii(data['hash'])
357 namespace = to_ascii(data['namespace'])
358 isolate_server = to_ascii(data['storage'])
359 if not file_path.is_url(isolate_server):
360 raise ValueError()
361 return (isolate_server, namespace, isolated_hash)
362 except (KeyError, ValueError):
363 logging.warning(
364 'Unexpected value of run_isolated_out_hack: %s', match.group(1))
365 return None
366
367
368def retrieve_results(
369 base_url, task_key, timeout, should_stop, output_collector):
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700370 """Retrieves results for a single task_key.
371
372 Returns a dict with results on success or None on failure or timeout.
373 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000374 assert isinstance(timeout, float), timeout
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500375 params = [('r', task_key)]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000376 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700377 started = now()
378 deadline = started + timeout if timeout else None
379 attempt = 0
380
381 while not should_stop.is_set():
382 attempt += 1
383
384 # Waiting for too long -> give up.
385 current_time = now()
386 if deadline and current_time >= deadline:
387 logging.error('retrieve_results(%s) timed out on attempt %d',
388 base_url, attempt)
389 return None
390
391 # Do not spin too fast. Spin faster at the beginning though.
392 # Start with 1 sec delay and for each 30 sec of waiting add another second
393 # of delay, until hitting 15 sec ceiling.
394 if attempt > 1:
395 max_delay = min(15, 1 + (current_time - started) / 30.0)
396 delay = min(max_delay, deadline - current_time) if deadline else max_delay
397 if delay > 0:
398 logging.debug('Waiting %.1f sec before retrying', delay)
399 should_stop.wait(delay)
400 if should_stop.is_set():
401 return None
402
403 # Disable internal retries in net.url_read, since we are doing retries
404 # ourselves. Do not use retry_404 so should_stop is polled more often.
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000405 response = net.url_read(result_url, retry_404=False, retry_50x=False)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700406
407 # Request failed. Try again.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000408 if response is None:
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700409 continue
410
411 # Got some response, ensure it is JSON dict, retry if not.
412 try:
413 result = json.loads(response) or {}
414 if not isinstance(result, dict):
415 raise ValueError()
416 except (ValueError, TypeError):
417 logging.warning(
418 'Received corrupted or invalid data for task_key %s, retrying: %r',
419 task_key, response)
420 continue
421
422 # Swarming server uses non-empty 'output' value as a flag that task has
423 # finished. How to wait for tasks that produce no output is a mystery.
424 if result.get('output'):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700425 # Record the result, try to fetch attached output files (if any).
426 if output_collector:
427 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
428 output_collector.process_shard_result(result)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700429 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000430
431
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700432def yield_results(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 swarm_base_url, task_keys, timeout, max_threads,
434 print_status_updates, output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500435 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000436
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700437 Duplicate shards are ignored. Shards are yielded in order of completion.
438 Timed out shards are NOT yielded at all. Caller can compare number of yielded
439 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000440
441 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500442 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 +0000443 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500444
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 output_collector is an optional instance of TaskOutputCollector that will be
446 used to fetch files produced by a task from isolate server to the local disk.
447
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500448 Yields:
449 (index, result). In particular, 'result' is defined as the
450 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000451 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000452 number_threads = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500453 min(max_threads, len(task_keys)) if max_threads else len(task_keys))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700454 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700455 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700456
maruel@chromium.org0437a732013-08-27 16:05:52 +0000457 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
458 try:
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700459 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500460 for task_key in task_keys:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000461 pool.add_task(
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700462 0, results_channel.wrap_task(retrieve_results),
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700463 swarm_base_url, task_key, timeout, should_stop, output_collector)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700464
465 # Wait for all of them to finish.
466 shards_remaining = range(len(task_keys))
467 active_task_count = len(task_keys)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700468 while active_task_count:
469 try:
470 result = results_channel.pull(timeout=STATUS_UPDATE_INTERVAL)
471 except threading_utils.TaskChannel.Timeout:
472 if print_status_updates:
473 print(
474 'Waiting for results from the following shards: %s' %
475 ', '.join(map(str, shards_remaining)))
476 sys.stdout.flush()
477 continue
478 except Exception:
479 logging.exception('Unexpected exception in retrieve_results')
480 result = None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700481
482 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700483 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000484 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500485 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000486 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700487
maruel@chromium.org0437a732013-08-27 16:05:52 +0000488 shard_index = result['config_instance_index']
489 if shard_index in shards_remaining:
490 shards_remaining.remove(shard_index)
491 yield shard_index, result
492 else:
493 logging.warning('Ignoring duplicate shard index %d', shard_index)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494
maruel@chromium.org0437a732013-08-27 16:05:52 +0000495 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700496 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000497 should_stop.set()
498
499
500def chromium_setup(manifest):
501 """Sets up the commands to run.
502
503 Highly chromium specific.
504 """
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000505 # Add uncompressed zip here. It'll be compressed as part of the package sent
506 # to Swarming server.
507 run_test_name = 'run_isolated.zip'
508 manifest.bundle.add_buffer(run_test_name,
509 run_isolated.get_as_zip_package().zip_into_buffer(compress=False))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000510
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000511 cleanup_script_name = 'swarm_cleanup.py'
512 manifest.bundle.add_file(os.path.join(TOOLS_PATH, cleanup_script_name),
513 cleanup_script_name)
514
maruel@chromium.org0437a732013-08-27 16:05:52 +0000515 run_cmd = [
516 'python', run_test_name,
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000517 '--hash', manifest.isolated_hash,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500518 '--namespace', manifest.namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000519 ]
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500520 if file_path.is_url(manifest.isolate_server):
521 run_cmd.extend(('--isolate-server', manifest.isolate_server))
522 else:
523 run_cmd.extend(('--indir', manifest.isolate_server))
524
maruel@chromium.org0437a732013-08-27 16:05:52 +0000525 if manifest.verbose or manifest.profile:
526 # Have it print the profiling section.
527 run_cmd.append('--verbose')
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700528
529 # Pass all extra args for run_isolated.py, it will pass them to the command.
530 if manifest.extra_args:
531 run_cmd.append('--')
532 run_cmd.extend(manifest.extra_args)
533
maruel@chromium.org0437a732013-08-27 16:05:52 +0000534 manifest.add_task('Run Test', run_cmd)
535
536 # Clean up
537 manifest.add_task('Clean Up', ['python', cleanup_script_name])
538
539
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500540def googletest_setup(env, shards):
541 """Sets googletest specific environment variables."""
542 if shards > 1:
543 env = env.copy()
544 env['GTEST_SHARD_INDEX'] = '%(instance_index)s'
545 env['GTEST_TOTAL_SHARDS'] = '%(num_instances)s'
546 return env
547
548
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500549def archive(isolate_server, namespace, isolated, algo, verbose):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000550 """Archives a .isolated and all the dependencies on the CAC."""
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500551 logging.info('archive(%s, %s, %s)', isolate_server, namespace, isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000552 tempdir = None
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500553 if file_path.is_url(isolate_server):
554 command = 'archive'
555 flag = '--isolate-server'
556 else:
557 command = 'hashtable'
558 flag = '--outdir'
559
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500560 print('Archiving: %s' % isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000561 try:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000562 cmd = [
563 sys.executable,
564 os.path.join(ROOT_DIR, 'isolate.py'),
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500565 command,
566 flag, isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500567 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000568 '--isolated', isolated,
569 ]
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000570 cmd.extend(['--verbose'] * verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000571 logging.info(' '.join(cmd))
572 if subprocess.call(cmd, verbose):
573 return
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000574 return isolateserver.hash_file(isolated, algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000575 finally:
576 if tempdir:
577 shutil.rmtree(tempdir)
578
579
580def process_manifest(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700581 swarming, isolate_server, namespace, isolated_hash, task_name, extra_args,
582 shards, dimensions, env, working_dir, deadline, verbose, profile, priority):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500583 """Processes the manifest file and send off the swarming task request."""
maruel@chromium.org0437a732013-08-27 16:05:52 +0000584 try:
585 manifest = Manifest(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500586 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500587 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500588 isolated_hash=isolated_hash,
589 task_name=task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700590 extra_args=extra_args,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500591 shards=shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500592 dimensions=dimensions,
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -0500593 env=env,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500594 working_dir=working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400595 deadline=deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500596 verbose=verbose,
597 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800598 priority=priority)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000599 except ValueError as e:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500600 tools.report_error('Unable to process %s: %s' % (task_name, e))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000601 return 1
602
603 chromium_setup(manifest)
604
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500605 logging.info('Zipping up files...')
606 if not zip_and_upload(manifest):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000607 return 1
608
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500609 logging.info('Server: %s', swarming)
610 logging.info('Task name: %s', task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500611 trigger_url = swarming + '/test'
maruel@chromium.org0437a732013-08-27 16:05:52 +0000612 manifest_text = manifest.to_json()
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500613 result = net.url_read(trigger_url, data={'request': manifest_text})
maruel@chromium.org0437a732013-08-27 16:05:52 +0000614 if not result:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000615 tools.report_error(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500616 'Failed to trigger task %s\n%s' % (task_name, trigger_url))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617 return 1
618 try:
vadimsh@chromium.orgf24e5c32013-10-11 21:16:21 +0000619 json.loads(result)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000620 except (ValueError, TypeError) as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000621 msg = '\n'.join((
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500622 'Failed to trigger task %s' % task_name,
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000623 'Manifest: %s' % manifest_text,
624 'Bad response: %s' % result,
625 str(e)))
626 tools.report_error(msg)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000627 return 1
628 return 0
629
630
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500631def isolated_to_hash(isolate_server, namespace, arg, algo, verbose):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500632 """Archives a .isolated file if needed.
633
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500634 Returns the file hash to trigger and a bool specifying if it was a file (True)
635 or a hash (False).
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500636 """
637 if arg.endswith('.isolated'):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500638 file_hash = archive(isolate_server, namespace, arg, algo, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500639 if not file_hash:
640 tools.report_error('Archival failure %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500641 return None, True
642 return file_hash, True
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500643 elif isolateserver.is_valid_hash(arg, algo):
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500644 return arg, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500645 else:
646 tools.report_error('Invalid hash %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500647 return None, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500648
649
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650def trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500651 swarming,
652 isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500653 namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500654 file_hash_or_isolated,
655 task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700656 extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500657 shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500658 dimensions,
659 env,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500660 working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400661 deadline,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000662 verbose,
663 profile,
664 priority):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500665 """Sends off the hash swarming task requests."""
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500666 file_hash, is_file = isolated_to_hash(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500667 isolate_server, namespace, file_hash_or_isolated, hashlib.sha1, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500668 if not file_hash:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500669 return 1, ''
670 if not task_name:
671 # If a file name was passed, use its base name of the isolated hash.
672 # Otherwise, use user name as an approximation of a task name.
673 if is_file:
674 key = os.path.splitext(os.path.basename(file_hash_or_isolated))[0]
675 else:
676 key = getpass.getuser()
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700677 task_name = '%s/%s/%s/%d' % (
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500678 key,
679 '_'.join('%s=%s' % (k, v) for k, v in sorted(dimensions.iteritems())),
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700680 file_hash,
681 now() * 1000)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500682
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500683 env = googletest_setup(env, shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500684 # TODO(maruel): It should first create a request manifest object, then pass
685 # it to a function to zip, archive and trigger.
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500686 result = process_manifest(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500687 swarming=swarming,
688 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500689 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500690 isolated_hash=file_hash,
691 task_name=task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700692 extra_args=extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500693 shards=shards,
694 dimensions=dimensions,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400695 deadline=deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500696 env=env,
697 working_dir=working_dir,
698 verbose=verbose,
699 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800700 priority=priority)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500701 return result, task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +0000702
703
704def decorate_shard_output(result, shard_exit_code):
705 """Returns wrapped output for swarming task shard."""
706 tag = 'index %s (machine tag: %s, id: %s)' % (
707 result['config_instance_index'],
708 result['machine_id'],
709 result.get('machine_tag', 'unknown'))
710 return (
711 '\n'
712 '================================================================\n'
713 'Begin output from shard %s\n'
714 '================================================================\n'
715 '\n'
716 '%s'
717 '================================================================\n'
718 'End output from shard %s. Return %d\n'
719 '================================================================\n'
720 ) % (tag, result['output'] or NO_OUTPUT_FOUND, tag, shard_exit_code)
721
722
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700723def collect(
724 url, task_name, timeout, decorate, print_status_updates, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500725 """Retrieves results of a Swarming task."""
726 logging.info('Collecting %s', task_name)
727 task_keys = get_task_keys(url, task_name)
728 if not task_keys:
729 raise Failure('No task keys to get results with.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000730
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731 # Collect output files only if explicitly asked with --task-output-dir option.
732 if task_output_dir:
733 output_collector = TaskOutputCollector(
734 task_output_dir, task_name, len(task_keys))
735 else:
736 output_collector = None
737
maruel@chromium.org9c1c7b52013-08-28 19:04:36 +0000738 exit_code = None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700739 seen_shards = set()
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700740
741 try:
742 for index, output in yield_results(
743 url, task_keys, timeout, None, print_status_updates, output_collector):
744 seen_shards.add(index)
745 shard_exit_codes = (output['exit_codes'] or '1').split(',')
746 shard_exit_code = max(int(i) for i in shard_exit_codes)
747 if decorate:
748 print decorate_shard_output(output, shard_exit_code)
749 else:
750 print(
751 '%s/%s: %s' % (
752 output['machine_id'],
753 output['machine_tag'],
754 output['exit_codes']))
755 print(''.join(' %s\n' % l for l in output['output'].splitlines()))
756 exit_code = exit_code or shard_exit_code
757 finally:
758 if output_collector:
759 output_collector.finalize()
760
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700761 if len(seen_shards) != len(task_keys):
762 missing_shards = [x for x in range(len(task_keys)) if x not in seen_shards]
763 print >> sys.stderr, ('Results from some shards are missing: %s' %
764 ', '.join(map(str, missing_shards)))
765 exit_code = exit_code or 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700766
maruel@chromium.org9c1c7b52013-08-28 19:04:36 +0000767 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000768
769
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400770def add_filter_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500771 parser.filter_group = tools.optparse.OptionGroup(parser, 'Filtering slaves')
772 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500773 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500774 dest='dimensions', metavar='FOO bar',
775 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500776 parser.add_option_group(parser.filter_group)
777
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400778
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400779def process_filter_options(parser, options):
780 options.dimensions = dict(options.dimensions)
781 if not options.dimensions:
782 parser.error('Please at least specify one --dimension')
783
784
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400785def add_trigger_options(parser):
786 """Adds all options to trigger a task on Swarming."""
787 isolateserver.add_isolate_server_options(parser, True)
788 add_filter_options(parser)
789
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500790 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
791 parser.task_group.add_option(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500792 '-w', '--working-dir', default='swarm_tests',
793 help='Working directory on the swarming slave side. default: %default.')
794 parser.task_group.add_option(
795 '--working_dir', help=tools.optparse.SUPPRESS_HELP)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500796 parser.task_group.add_option(
797 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
798 help='environment variables to set')
799 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500800 '--priority', type='int', default=100,
801 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500802 parser.task_group.add_option(
803 '--shards', type='int', default=1, help='number of shards to use')
804 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500805 '-T', '--task-name',
806 help='Display name of the task. It uniquely identifies the task. '
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700807 'Defaults to <base_name>/<dimensions>/<isolated hash>/<timestamp> '
808 'if an isolated file is provided, if a hash is provided, it '
809 'defaults to <user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400810 parser.task_group.add_option(
811 '--deadline', type='int', default=6*60*60,
812 help='Seconds to allow the task to be pending for a bot to run before '
813 'this task request expires.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500814 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500815 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500816 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000817 '--profile', action='store_true',
818 default=bool(os.environ.get('ISOLATE_DEBUG')),
819 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000820
821
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500822def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500823 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500824 if len(args) != 1:
825 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400826 process_filter_options(parser, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000827
828
829def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500830 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000831 '-t', '--timeout',
832 type='float',
833 default=DEFAULT_SHARD_WAIT_TIME,
834 help='Timeout to wait for result, set to 0 for no timeout; default: '
835 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500836 parser.group_logging.add_option(
837 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700838 parser.group_logging.add_option(
839 '--print-status-updates', action='store_true',
840 help='Print periodic status updates')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700841 parser.task_output_group = tools.optparse.OptionGroup(parser, 'Task output')
842 parser.task_output_group.add_option(
843 '--task-output-dir',
844 help='Directory to put task results into. When the task finishes, this '
845 'directory contains <task-output-dir>/summary.json file with '
846 'a summary of task results across all shards, and per-shard '
847 'directory with output files produced by a shard: '
848 '<task-output-dir>/<zero-based-shard-index>/')
849 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000850
851
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700852def extract_isolated_command_extra_args(args):
853 try:
854 index = args.index('--')
855 except ValueError:
856 return (args, [])
857 return (args[:index], args[index+1:])
858
859
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500860@subcommand.usage('task_name')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000861def CMDcollect(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500862 """Retrieves results of a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000863
864 The result can be in multiple part if the execution was sharded. It can
865 potentially have retries.
866 """
867 add_collect_options(parser)
868 (options, args) = parser.parse_args(args)
869 if not args:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500870 parser.error('Must specify one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000871 elif len(args) > 1:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500872 parser.error('Must specify only one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000873
874 try:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700875 return collect(
876 options.swarming,
877 args[0],
878 options.timeout,
879 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700880 options.print_status_updates,
881 options.task_output_dir)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000882 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000883 tools.report_error(e)
884 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000885
886
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400887def CMDquery(parser, args):
888 """Returns information about the bots connected to the Swarming server."""
889 add_filter_options(parser)
890 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400891 '--dead-only', action='store_true',
892 help='Only print dead bots, useful to reap them and reimage broken bots')
893 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400894 '-k', '--keep-dead', action='store_true',
895 help='Do not filter out dead bots')
896 parser.filter_group.add_option(
897 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400898 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400899 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400900
901 if options.keep_dead and options.dead_only:
902 parser.error('Use only one of --keep-dead and --dead-only')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400903 service = net.get_http_service(options.swarming)
904 data = service.json_request('GET', '/swarming/api/v1/bots')
905 if data is None:
906 print >> sys.stderr, 'Failed to access %s' % options.swarming
907 return 1
908 timeout = datetime.timedelta(seconds=data['machine_death_timeout'])
909 utcnow = datetime.datetime.utcnow()
910 for machine in natsort.natsorted(data['machines'], key=lambda x: x['tag']):
911 last_seen = datetime.datetime.strptime(
912 machine['last_seen'], '%Y-%m-%d %H:%M:%S')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400913 is_dead = utcnow - last_seen > timeout
914 if options.dead_only:
915 if not is_dead:
916 continue
917 elif not options.keep_dead and is_dead:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400918 continue
919
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400920 # If the user requested to filter on dimensions, ensure the bot has all the
921 # dimensions requested.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400922 dimensions = machine['dimensions']
923 for key, value in options.dimensions:
924 if key not in dimensions:
925 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400926 # A bot can have multiple value for a key, for example,
927 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
928 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400929 if isinstance(dimensions[key], list):
930 if value not in dimensions[key]:
931 break
932 else:
933 if value != dimensions[key]:
934 break
935 else:
936 print machine['tag']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400937 if not options.bare:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400938 print ' %s' % dimensions
939 return 0
940
941
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700942@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000943def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500944 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000945
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500946 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000947 """
948 add_trigger_options(parser)
949 add_collect_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700950 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000951 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500952 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000953
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500954 try:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500955 result, task_name = trigger(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500956 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500957 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500958 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500959 file_hash_or_isolated=args[0],
960 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700961 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500962 shards=options.shards,
963 dimensions=options.dimensions,
964 env=dict(options.env),
965 working_dir=options.working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400966 deadline=options.deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500967 verbose=options.verbose,
968 profile=options.profile,
969 priority=options.priority)
970 except Failure as e:
971 tools.report_error(
972 'Failed to trigger %s(%s): %s' %
973 (options.task_name, args[0], e.args[0]))
974 return 1
975 if result:
976 tools.report_error('Failed to trigger the task.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000977 return result
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500978 if task_name != options.task_name:
979 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500980 try:
981 return collect(
982 options.swarming,
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500983 task_name,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500984 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700985 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700986 options.print_status_updates,
987 options.task_output_dir)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500988 except Failure as e:
989 tools.report_error(e)
990 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000991
992
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700993@subcommand.usage("(hash|isolated) [-- extra_args]")
maruel@chromium.org0437a732013-08-27 16:05:52 +0000994def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500995 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000996
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500997 Accepts either the hash (sha1) of a .isolated file already uploaded or the
998 path to an .isolated file to archive, packages it if needed and sends a
999 Swarming manifest file to the Swarming server.
1000
1001 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001002
1003 Passes all extra arguments provided after '--' as additional command line
1004 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001005 """
1006 add_trigger_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001007 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001008 options, args = parser.parse_args(args)
1009 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001010
1011 try:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001012 result, task_name = trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001013 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001014 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001015 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001016 file_hash_or_isolated=args[0],
1017 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001018 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001019 shards=options.shards,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001020 dimensions=options.dimensions,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -05001021 env=dict(options.env),
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001022 working_dir=options.working_dir,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -04001023 deadline=options.deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001024 verbose=options.verbose,
1025 profile=options.profile,
1026 priority=options.priority)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001027 if task_name != options.task_name and not result:
1028 print('Triggered task: %s' % task_name)
1029 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +00001030 except Failure as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001031 tools.report_error(e)
1032 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001033
1034
1035class OptionParserSwarming(tools.OptionParserWithLogging):
1036 def __init__(self, **kwargs):
1037 tools.OptionParserWithLogging.__init__(
1038 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001039 self.server_group = tools.optparse.OptionGroup(self, 'Server')
1040 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001041 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001042 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001043 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001044 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001045 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001046
1047 def parse_args(self, *args, **kwargs):
1048 options, args = tools.OptionParserWithLogging.parse_args(
1049 self, *args, **kwargs)
1050 options.swarming = options.swarming.rstrip('/')
1051 if not options.swarming:
1052 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001053 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001054 return options, args
1055
1056
1057def main(args):
1058 dispatcher = subcommand.CommandDispatcher(__name__)
1059 try:
1060 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001061 except Exception as e:
1062 tools.report_error(e)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001063 return 1
1064
1065
1066if __name__ == '__main__':
1067 fix_encoding.fix_encoding()
1068 tools.disable_buffering()
1069 colorama.init()
1070 sys.exit(main(sys.argv[1:]))