blob: 2708bdc1669c43e9d2e1a5b98829053b83510d1b [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 Shtayurac8437bf2014-07-09 19:45:36 -07008__version__ = '0.4.12'
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
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040031from utils import on_error
maruel@chromium.org0437a732013-08-27 16:05:52 +000032from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000033from utils import tools
34from utils import zip_package
maruel@chromium.org0437a732013-08-27 16:05:52 +000035
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036import auth
maruel@chromium.org7b844a62013-09-17 13:04:59 +000037import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000038import run_isolated
39
40
41ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
42TOOLS_PATH = os.path.join(ROOT_DIR, 'tools')
43
44
maruel@chromium.org0437a732013-08-27 16:05:52 +000045# The default time to wait for a shard to finish running.
csharp@chromium.org24758492013-08-28 19:10:54 +000046DEFAULT_SHARD_WAIT_TIME = 80 * 60.
maruel@chromium.org0437a732013-08-27 16:05:52 +000047
Vadim Shtayura86a2cef2014-04-18 11:13:39 -070048# How often to print status updates to stdout in 'collect'.
49STATUS_UPDATE_INTERVAL = 15 * 60.
50
maruel@chromium.org0437a732013-08-27 16:05:52 +000051
52NO_OUTPUT_FOUND = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -050053 'No output produced by the task, it may have failed to run.\n'
maruel@chromium.org0437a732013-08-27 16:05:52 +000054 '\n')
55
56
maruel@chromium.org0437a732013-08-27 16:05:52 +000057class Failure(Exception):
58 """Generic failure."""
59 pass
60
61
62class Manifest(object):
Vadim Shtayurab450c602014-05-12 19:23:25 -070063 """Represents a Swarming task manifest."""
maruel@chromium.org0437a732013-08-27 16:05:52 +000064
maruel@chromium.org0437a732013-08-27 16:05:52 +000065 def __init__(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070066 self, isolate_server, namespace, isolated_hash, task_name, extra_args,
Marc-Antoine Ruelaea50652014-06-12 14:23:48 -040067 env, dimensions, deadline, verbose, profile,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070068 priority):
maruel@chromium.org0437a732013-08-27 16:05:52 +000069 """Populates a manifest object.
70 Args:
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050071 isolate_server - isolate server url.
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050072 namespace - isolate server namespace to use.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070073 isolated_hash - the manifest's sha-1 that the slave is going to fetch.
74 task_name - the name to give the task request.
75 extra_args - additional arguments to pass to isolated command.
Marc-Antoine Ruel05dab5e2013-11-06 15:06:47 -050076 env - environment variables to set.
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -050077 dimensions - dimensions to filter the task on.
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -040078 deadline - maximum pending time before this task expires.
maruel@chromium.org0437a732013-08-27 16:05:52 +000079 verbose - if True, have the slave print more details.
80 profile - if True, have the slave print more timing data.
maruel@chromium.org7b844a62013-09-17 13:04:59 +000081 priority - int between 0 and 1000, lower the higher priority.
maruel@chromium.org0437a732013-08-27 16:05:52 +000082 """
Marc-Antoine Ruela7049872013-11-05 19:28:35 -050083 self.isolate_server = isolate_server
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -050084 self.namespace = namespace
maruel@chromium.org814d23f2013-10-01 19:08:00 +000085 self.isolated_hash = isolated_hash
Vadim Shtayurab450c602014-05-12 19:23:25 -070086 self.task_name = task_name
Vadim Shtayuraae8085b2014-05-02 17:13:10 -070087 self.extra_args = tuple(extra_args or [])
Vadim Shtayurab450c602014-05-12 19:23:25 -070088 self.env = env.copy()
89 self.dimensions = dimensions.copy()
Vadim Shtayurab450c602014-05-12 19:23:25 -070090 self.deadline = deadline
maruel@chromium.org0437a732013-08-27 16:05:52 +000091 self.verbose = bool(verbose)
92 self.profile = bool(profile)
93 self.priority = priority
maruel@chromium.org0437a732013-08-27 16:05:52 +000094 self._tasks = []
Vadim Shtayurab450c602014-05-12 19:23:25 -070095 self._files = []
maruel@chromium.org0437a732013-08-27 16:05:52 +000096
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -040097 def add_task(self, task_name, actions, time_out=2*60*60):
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -050098 """Appends a new task as a TestObject to the swarming manifest file.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -050099
100 Tasks cannot be added once the manifest was uploaded.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500101
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400102 By default, command will be killed after 2 hours of execution.
103
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500104 See TestObject in services/swarming/src/common/test_request_message.py for
105 the valid format.
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500106 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000107 self._tasks.append(
108 {
109 'action': actions,
110 'decorate_output': self.verbose,
111 'test_name': task_name,
Marc-Antoine Ruelaf78a902014-03-20 10:42:49 -0400112 'hard_time_out': time_out,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000113 })
114
Vadim Shtayurab450c602014-05-12 19:23:25 -0700115 def add_bundled_file(self, file_name, file_url):
116 """Appends a file to the manifest.
117
118 File will be downloaded and extracted by the swarm bot before launching the
119 task.
120 """
121 self._files.append([file_url, file_name])
122
maruel@chromium.org0437a732013-08-27 16:05:52 +0000123 def to_json(self):
124 """Exports the current configuration into a swarm-readable manifest file.
125
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500126 The actual serialization format is defined as a TestCase object as described
127 in services/swarming/src/common/test_request_message.py
maruel@chromium.org0437a732013-08-27 16:05:52 +0000128 """
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500129 request = {
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500130 'cleanup': 'root',
maruel@chromium.org0437a732013-08-27 16:05:52 +0000131 'configurations': [
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500132 # Is a TestConfiguration.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000133 {
Marc-Antoine Ruel5d799192013-11-06 15:20:39 -0500134 'config_name': 'isolated',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700135 'deadline_to_run': self.deadline,
136 'dimensions': self.dimensions,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500137 'priority': self.priority,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000138 },
139 ],
Vadim Shtayurab450c602014-05-12 19:23:25 -0700140 'data': self._files,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700141 'env_vars': self.env,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700142 'test_case_name': self.task_name,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500143 'tests': self._tasks,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000144 }
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500145 return json.dumps(request, sort_keys=True, separators=(',',':'))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000146
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500147
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700148class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700149 """Assembles task execution summary (for --task-summary-json output).
150
151 Optionally fetches task outputs from isolate server to local disk (used when
152 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700153
154 This object is shared among multiple threads running 'retrieve_results'
155 function, in particular they call 'process_shard_result' method in parallel.
156 """
157
158 def __init__(self, task_output_dir, task_name, shard_count):
159 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
160
161 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700162 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700163 task_name: name of the swarming task results belong to.
164 shard_count: expected number of task shards.
165 """
166 self.task_output_dir = task_output_dir
167 self.task_name = task_name
168 self.shard_count = shard_count
169
170 self._lock = threading.Lock()
171 self._per_shard_results = {}
172 self._storage = None
173
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700174 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700175 os.makedirs(self.task_output_dir)
176
Vadim Shtayurab450c602014-05-12 19:23:25 -0700177 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700178 """Stores results of a single task shard, fetches output files if necessary.
179
180 Called concurrently from multiple threads.
181 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700182 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700183 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700184 if shard_index < 0 or shard_index >= self.shard_count:
185 logging.warning(
186 'Shard index %d is outside of expected range: [0; %d]',
187 shard_index, self.shard_count - 1)
188 return
189
190 # Store result dict of that shard, ignore results we've already seen.
191 with self._lock:
192 if shard_index in self._per_shard_results:
193 logging.warning('Ignoring duplicate shard index %d', shard_index)
194 return
195 self._per_shard_results[shard_index] = result
196
197 # Fetch output files if necessary.
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700198 if self.task_output_dir:
199 isolated_files_location = extract_output_files_location(result['output'])
200 if isolated_files_location:
201 isolate_server, namespace, isolated_hash = isolated_files_location
202 storage = self._get_storage(isolate_server, namespace)
203 if storage:
204 # Output files are supposed to be small and they are not reused across
205 # tasks. So use MemoryCache for them instead of on-disk cache. Make
206 # files writable, so that calling script can delete them.
207 isolateserver.fetch_isolated(
208 isolated_hash,
209 storage,
210 isolateserver.MemoryCache(file_mode_mask=0700),
211 os.path.join(self.task_output_dir, str(shard_index)),
212 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700213
214 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700215 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700216 with self._lock:
217 # Write an array of shard results with None for missing shards.
218 summary = {
219 'task_name': self.task_name,
220 'shards': [
221 self._per_shard_results.get(i) for i in xrange(self.shard_count)
222 ],
223 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700224 # Write summary.json to task_output_dir as well.
225 if self.task_output_dir:
226 tools.write_json(
227 os.path.join(self.task_output_dir, 'summary.json'),
228 summary,
229 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700230 if self._storage:
231 self._storage.close()
232 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700233 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700234
235 def _get_storage(self, isolate_server, namespace):
236 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700237 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700238 with self._lock:
239 if not self._storage:
240 self._storage = isolateserver.get_storage(isolate_server, namespace)
241 else:
242 # Shards must all use exact same isolate server and namespace.
243 if self._storage.location != isolate_server:
244 logging.error(
245 'Task shards are using multiple isolate servers: %s and %s',
246 self._storage.location, isolate_server)
247 return None
248 if self._storage.namespace != namespace:
249 logging.error(
250 'Task shards are using multiple namespaces: %s and %s',
251 self._storage.namespace, namespace)
252 return None
253 return self._storage
254
255
maruel@chromium.org0437a732013-08-27 16:05:52 +0000256def now():
257 """Exists so it can be mocked easily."""
258 return time.time()
259
260
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500261def get_task_keys(swarm_base_url, task_name):
262 """Returns the Swarming task key for each shards of task_name."""
263 key_data = urllib.urlencode([('name', task_name)])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000264 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data)
265
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000266 for _ in net.retry_loop(max_attempts=net.URL_OPEN_MAX_ATTEMPTS):
267 result = net.url_read(url, retry_404=True)
268 if result is None:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000269 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500270 'Error: Unable to find any task with the name, %s, on swarming server'
271 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000272
maruel@chromium.org0437a732013-08-27 16:05:52 +0000273 # TODO(maruel): Compare exact string.
274 if 'No matching' in result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500275 logging.warning('Unable to find any task with the name, %s, on swarming '
276 'server' % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000277 continue
278 return json.loads(result)
279
280 raise Failure(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500281 'Error: Unable to find any task with the name, %s, on swarming server'
282 % task_name)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000283
284
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700285def extract_output_files_location(task_log):
286 """Task log -> location of task output files to fetch.
287
288 TODO(vadimsh,maruel): Use side-channel to get this information.
289 See 'run_tha_test' in run_isolated.py for where the data is generated.
290
291 Returns:
292 Tuple (isolate server URL, namespace, isolated hash) on success.
293 None if information is missing or can not be parsed.
294 """
295 match = re.search(
296 r'\[run_isolated_out_hack\](.*)\[/run_isolated_out_hack\]',
297 task_log,
298 re.DOTALL)
299 if not match:
300 return None
301
302 def to_ascii(val):
303 if not isinstance(val, basestring):
304 raise ValueError()
305 return val.encode('ascii')
306
307 try:
308 data = json.loads(match.group(1))
309 if not isinstance(data, dict):
310 raise ValueError()
311 isolated_hash = to_ascii(data['hash'])
312 namespace = to_ascii(data['namespace'])
313 isolate_server = to_ascii(data['storage'])
314 if not file_path.is_url(isolate_server):
315 raise ValueError()
316 return (isolate_server, namespace, isolated_hash)
317 except (KeyError, ValueError):
318 logging.warning(
319 'Unexpected value of run_isolated_out_hack: %s', match.group(1))
320 return None
321
322
323def retrieve_results(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700324 base_url, shard_index, task_key, timeout, should_stop, output_collector):
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700325 """Retrieves results for a single task_key.
326
Vadim Shtayurab450c602014-05-12 19:23:25 -0700327 Returns:
328 <result dict> on success.
329 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700330 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000331 assert isinstance(timeout, float), timeout
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500332 params = [('r', task_key)]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000333 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700334 started = now()
335 deadline = started + timeout if timeout else None
336 attempt = 0
337
338 while not should_stop.is_set():
339 attempt += 1
340
341 # Waiting for too long -> give up.
342 current_time = now()
343 if deadline and current_time >= deadline:
344 logging.error('retrieve_results(%s) timed out on attempt %d',
345 base_url, attempt)
346 return None
347
348 # Do not spin too fast. Spin faster at the beginning though.
349 # Start with 1 sec delay and for each 30 sec of waiting add another second
350 # of delay, until hitting 15 sec ceiling.
351 if attempt > 1:
352 max_delay = min(15, 1 + (current_time - started) / 30.0)
353 delay = min(max_delay, deadline - current_time) if deadline else max_delay
354 if delay > 0:
355 logging.debug('Waiting %.1f sec before retrying', delay)
356 should_stop.wait(delay)
357 if should_stop.is_set():
358 return None
359
360 # Disable internal retries in net.url_read, since we are doing retries
361 # ourselves. Do not use retry_404 so should_stop is polled more often.
vadimsh@chromium.org043b76d2013-09-12 16:15:13 +0000362 response = net.url_read(result_url, retry_404=False, retry_50x=False)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700363
364 # Request failed. Try again.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000365 if response is None:
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700366 continue
367
368 # Got some response, ensure it is JSON dict, retry if not.
369 try:
370 result = json.loads(response) or {}
371 if not isinstance(result, dict):
372 raise ValueError()
373 except (ValueError, TypeError):
374 logging.warning(
375 'Received corrupted or invalid data for task_key %s, retrying: %r',
376 task_key, response)
377 continue
378
379 # Swarming server uses non-empty 'output' value as a flag that task has
380 # finished. How to wait for tasks that produce no output is a mystery.
381 if result.get('output'):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 # Record the result, try to fetch attached output files (if any).
383 if output_collector:
384 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700385 output_collector.process_shard_result(shard_index, result)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700386 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000387
388
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700389def yield_results(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700390 swarm_base_url, task_keys, timeout, max_threads,
391 print_status_updates, output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500392 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000393
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700394 Duplicate shards are ignored. Shards are yielded in order of completion.
395 Timed out shards are NOT yielded at all. Caller can compare number of yielded
396 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000397
398 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500399 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 +0000400 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500401
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 output_collector is an optional instance of TaskOutputCollector that will be
403 used to fetch files produced by a task from isolate server to the local disk.
404
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500405 Yields:
406 (index, result). In particular, 'result' is defined as the
407 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000408 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000409 number_threads = (
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500410 min(max_threads, len(task_keys)) if max_threads else len(task_keys))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700411 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700412 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700413
maruel@chromium.org0437a732013-08-27 16:05:52 +0000414 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
415 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700416 # Adds a task to the thread pool to call 'retrieve_results' and return
417 # the results together with shard_index that produced them (as a tuple).
418 def enqueue_retrieve_results(shard_index, task_key):
419 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000420 pool.add_task(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700421 0, results_channel.wrap_task(task_fn),
422 swarm_base_url, shard_index, task_key, timeout,
423 should_stop, output_collector)
424
425 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
426 for shard_index, task_key in enumerate(task_keys):
427 enqueue_retrieve_results(shard_index, task_key)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700428
429 # Wait for all of them to finish.
430 shards_remaining = range(len(task_keys))
431 active_task_count = len(task_keys)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700432 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700433 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700434 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700435 shard_index, result = results_channel.pull(
436 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700437 except threading_utils.TaskChannel.Timeout:
438 if print_status_updates:
439 print(
440 'Waiting for results from the following shards: %s' %
441 ', '.join(map(str, shards_remaining)))
442 sys.stdout.flush()
443 continue
444 except Exception:
445 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700446
447 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700448 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000449 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500450 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000451 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700452
Vadim Shtayurab450c602014-05-12 19:23:25 -0700453 # Yield back results to the caller.
454 assert shard_index in shards_remaining
455 shards_remaining.remove(shard_index)
456 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700457
maruel@chromium.org0437a732013-08-27 16:05:52 +0000458 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700459 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000460 should_stop.set()
461
462
Vadim Shtayurab450c602014-05-12 19:23:25 -0700463def setup_run_isolated(manifest, bundle):
464 """Sets up the manifest to run an isolated task via run_isolated.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000465
Vadim Shtayurab450c602014-05-12 19:23:25 -0700466 Modifies |bundle| (by adding files) and |manifest| (by adding commands) in
467 place.
468
469 Args:
470 manifest: Manifest with swarm task definition.
471 bundle: ZipPackage with files that would be transfered to swarm bot.
472 If None, only |manifest| is modified (useful in tests).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000473 """
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000474 # Add uncompressed zip here. It'll be compressed as part of the package sent
475 # to Swarming server.
476 run_test_name = 'run_isolated.zip'
Vadim Shtayurab450c602014-05-12 19:23:25 -0700477 if bundle and run_test_name not in bundle.files:
478 bundle.add_buffer(
479 run_test_name,
480 run_isolated.get_as_zip_package().zip_into_buffer(compress=False))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000481
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000482 cleanup_script_name = 'swarm_cleanup.py'
Vadim Shtayurab450c602014-05-12 19:23:25 -0700483 if bundle and cleanup_script_name not in bundle.files:
484 bundle.add_file(
485 os.path.join(TOOLS_PATH, cleanup_script_name), cleanup_script_name)
vadimsh@chromium.org6b706212013-08-28 15:03:46 +0000486
maruel@chromium.org0437a732013-08-27 16:05:52 +0000487 run_cmd = [
488 'python', run_test_name,
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000489 '--hash', manifest.isolated_hash,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500490 '--namespace', manifest.namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000491 ]
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500492 if file_path.is_url(manifest.isolate_server):
493 run_cmd.extend(('--isolate-server', manifest.isolate_server))
494 else:
495 run_cmd.extend(('--indir', manifest.isolate_server))
496
maruel@chromium.org0437a732013-08-27 16:05:52 +0000497 if manifest.verbose or manifest.profile:
498 # Have it print the profiling section.
499 run_cmd.append('--verbose')
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700500
501 # Pass all extra args for run_isolated.py, it will pass them to the command.
502 if manifest.extra_args:
503 run_cmd.append('--')
504 run_cmd.extend(manifest.extra_args)
505
maruel@chromium.org0437a732013-08-27 16:05:52 +0000506 manifest.add_task('Run Test', run_cmd)
507
508 # Clean up
509 manifest.add_task('Clean Up', ['python', cleanup_script_name])
510
511
Vadim Shtayurab450c602014-05-12 19:23:25 -0700512def setup_googletest(env, shards, index):
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500513 """Sets googletest specific environment variables."""
514 if shards > 1:
515 env = env.copy()
Vadim Shtayurab450c602014-05-12 19:23:25 -0700516 env['GTEST_SHARD_INDEX'] = str(index)
517 env['GTEST_TOTAL_SHARDS'] = str(shards)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500518 return env
519
520
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500521def archive(isolate_server, namespace, isolated, algo, verbose):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000522 """Archives a .isolated and all the dependencies on the CAC."""
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500523 logging.info('archive(%s, %s, %s)', isolate_server, namespace, isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000524 tempdir = None
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500525 if file_path.is_url(isolate_server):
526 command = 'archive'
527 flag = '--isolate-server'
528 else:
529 command = 'hashtable'
530 flag = '--outdir'
531
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500532 print('Archiving: %s' % isolated)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000533 try:
maruel@chromium.org0437a732013-08-27 16:05:52 +0000534 cmd = [
535 sys.executable,
536 os.path.join(ROOT_DIR, 'isolate.py'),
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500537 command,
538 flag, isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500539 '--namespace', namespace,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000540 '--isolated', isolated,
541 ]
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000542 cmd.extend(['--verbose'] * verbose)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000543 logging.info(' '.join(cmd))
544 if subprocess.call(cmd, verbose):
545 return
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000546 return isolateserver.hash_file(isolated, algo)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000547 finally:
548 if tempdir:
549 shutil.rmtree(tempdir)
550
551
Vadim Shtayurab450c602014-05-12 19:23:25 -0700552def get_shard_task_name(task_name, shards, index):
553 """Returns a task name to use for a single shard of a task."""
554 if shards == 1:
555 return task_name
556 return '%s:%s:%s' % (task_name, shards, index)
557
558
559def upload_zip_bundle(isolate_server, bundle):
560 """Uploads a zip package to isolate storage and returns raw fetch URL.
561
562 Args:
563 isolate_server: URL of an isolate server.
564 bundle: instance of ZipPackage to upload.
565
566 Returns:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400567 URL to get the file from.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700568 """
569 # Swarming bot would need to be able to grab the file from the storage
570 # using raw HTTP GET. Use 'default' namespace so that the raw data returned
571 # to a bot is not zipped, since swarm_bot doesn't understand compressed
572 # data yet. This namespace have nothing to do with |namespace| passed to
573 # run_isolated.py that is used to store files for isolated task.
574 logging.info('Zipping up and uploading files...')
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400575 start_time = now()
576 isolate_item = isolateserver.BufferItem(
577 bundle.zip_into_buffer(), high_priority=True)
578 with isolateserver.get_storage(isolate_server, 'default') as storage:
579 uploaded = storage.upload_items([isolate_item])
580 bundle_url = storage.get_fetch_url(isolate_item)
581 elapsed = now() - start_time
Vadim Shtayurab450c602014-05-12 19:23:25 -0700582 if isolate_item in uploaded:
583 logging.info('Upload complete, time elapsed: %f', elapsed)
584 else:
585 logging.info('Zip file already on server, time elapsed: %f', elapsed)
586 return bundle_url
587
588
589def trigger_by_manifest(swarming, manifest):
590 """Given a task manifest, triggers it for execution on swarming.
591
592 Args:
593 swarming: URL of a swarming service.
594 manifest: instance of Manifest.
595
596 Returns:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400597 tuple(Task id, priority) on success. tuple(None, None) on failure.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700598 """
599 logging.info('Triggering: %s', manifest.task_name)
600 manifest_text = manifest.to_json()
601 result = net.url_read(swarming + '/test', data={'request': manifest_text})
602 if not result:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400603 on_error.report('Failed to trigger task %s' % manifest.task_name)
Vadim Shtayura1c024f72014-07-09 19:00:10 -0700604 return None, None
Vadim Shtayurab450c602014-05-12 19:23:25 -0700605 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400606 data = json.loads(result)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400607 except (ValueError, TypeError):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700608 msg = '\n'.join((
609 'Failed to trigger task %s' % manifest.task_name,
610 'Manifest: %s' % manifest_text,
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400611 'Bad response: %s' % result))
612 on_error.report(msg)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400613 return None, None
614 if not data:
615 return None, None
616 return data['test_keys'][0]['test_key'], data['priority']
Vadim Shtayurab450c602014-05-12 19:23:25 -0700617
618
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400619def abort_task(_swarming, _manifest):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700620 """Given a task manifest that was triggered, aborts its execution."""
621 # TODO(vadimsh): No supported by the server yet.
622
623
624def trigger_task_shards(
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700625 swarming, isolate_server, namespace, isolated_hash, task_name, extra_args,
Marc-Antoine Ruelaea50652014-06-12 14:23:48 -0400626 shards, dimensions, env, deadline, verbose, profile, priority):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400627 """Triggers multiple subtasks of a sharded task.
628
629 Returns:
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700630 Dict with task details, returned to caller as part of --dump-json output.
631 None in case of failure.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400632 """
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 # Collects all files that are necessary to bootstrap a task execution
634 # on the bot. Usually it includes self contained run_isolated.zip and
635 # a bunch of small other scripts. All heavy files are pulled
636 # by run_isolated.zip. Updated in 'setup_run_isolated'.
637 bundle = zip_package.ZipPackage(ROOT_DIR)
638
639 # Make a separate Manifest for each shard, put shard index and number of
640 # shards into env and subtask name.
641 manifests = []
642 for index in xrange(shards):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000643 manifest = Manifest(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500644 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500645 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500646 isolated_hash=isolated_hash,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700647 task_name=get_shard_task_name(task_name, shards, index),
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700648 extra_args=extra_args,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500649 dimensions=dimensions,
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 env=setup_googletest(env, shards, index),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400651 deadline=deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500652 verbose=verbose,
653 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800654 priority=priority)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700655 setup_run_isolated(manifest, bundle)
656 manifests.append(manifest)
657
658 # Upload zip bundle file to get its URL.
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400659 try:
660 bundle_url = upload_zip_bundle(isolate_server, bundle)
661 except (IOError, OSError):
662 on_error.report('Failed to upload the zip file for task %s' % task_name)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400663 return None, None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000664
Vadim Shtayurab450c602014-05-12 19:23:25 -0700665 # Attach that file to all manifests.
666 for manifest in manifests:
667 manifest.add_bundled_file('swarm_data.zip', bundle_url)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000668
Vadim Shtayurab450c602014-05-12 19:23:25 -0700669 # Trigger all the subtasks.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400670 tasks = {}
671 priority_warning = False
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700672 for index, manifest in enumerate(manifests):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400673 task_id, priority = trigger_by_manifest(swarming, manifest)
674 if not task_id:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700675 break
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400676 if not priority_warning and priority != manifest.priority:
677 priority_warning = True
678 print >> sys.stderr, 'Priority was reset to %s' % priority
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700679 tasks[manifest.task_name] = {
680 'shard_index': index,
681 'task_id': task_id,
682 'view_url': '%s/user/task/%s' % (swarming, task_id),
683 }
Vadim Shtayurab450c602014-05-12 19:23:25 -0700684
685 # Some shards weren't triggered. Abort everything.
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400686 if len(tasks) != len(manifests):
687 if tasks:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700688 print >> sys.stderr, 'Not all shards were triggered'
Vadim Shtayuraf27448e2014-06-26 11:35:05 -0700689 for task_dict in tasks.itervalues():
690 abort_task(swarming, task_dict['task_id'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400691 return None
maruel@chromium.org0437a732013-08-27 16:05:52 +0000692
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400693 return tasks
maruel@chromium.org0437a732013-08-27 16:05:52 +0000694
695
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500696def isolated_to_hash(isolate_server, namespace, arg, algo, verbose):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500697 """Archives a .isolated file if needed.
698
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500699 Returns the file hash to trigger and a bool specifying if it was a file (True)
700 or a hash (False).
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500701 """
702 if arg.endswith('.isolated'):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500703 file_hash = archive(isolate_server, namespace, arg, algo, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500704 if not file_hash:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400705 on_error.report('Archival failure %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500706 return None, True
707 return file_hash, True
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500708 elif isolateserver.is_valid_hash(arg, algo):
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500709 return arg, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500710 else:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400711 on_error.report('Invalid hash %s' % arg)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500712 return None, False
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500713
714
maruel@chromium.org0437a732013-08-27 16:05:52 +0000715def trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -0500716 swarming,
717 isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500718 namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500719 file_hash_or_isolated,
720 task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700721 extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500722 shards,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500723 dimensions,
724 env,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400725 deadline,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000726 verbose,
727 profile,
728 priority):
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400729 """Sends off the hash swarming task requests.
730
731 Returns:
732 tuple(dict(task_name: task_id), base task name). The dict of tasks is None
733 in case of failure.
734 """
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500735 file_hash, is_file = isolated_to_hash(
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500736 isolate_server, namespace, file_hash_or_isolated, hashlib.sha1, verbose)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500737 if not file_hash:
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500738 return 1, ''
739 if not task_name:
740 # If a file name was passed, use its base name of the isolated hash.
741 # Otherwise, use user name as an approximation of a task name.
742 if is_file:
743 key = os.path.splitext(os.path.basename(file_hash_or_isolated))[0]
744 else:
745 key = getpass.getuser()
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700746 task_name = '%s/%s/%s/%d' % (
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500747 key,
748 '_'.join('%s=%s' % (k, v) for k, v in sorted(dimensions.iteritems())),
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700749 file_hash,
750 now() * 1000)
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500751
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400752 tasks = trigger_task_shards(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500753 swarming=swarming,
754 isolate_server=isolate_server,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500755 namespace=namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500756 isolated_hash=file_hash,
757 task_name=task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700758 extra_args=extra_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500759 shards=shards,
760 dimensions=dimensions,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400761 deadline=deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500762 env=env,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500763 verbose=verbose,
764 profile=profile,
Vadim Shtayurabcff74f2014-02-27 16:19:34 -0800765 priority=priority)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -0400766 return tasks, task_name
maruel@chromium.org0437a732013-08-27 16:05:52 +0000767
768
Vadim Shtayurab450c602014-05-12 19:23:25 -0700769def decorate_shard_output(shard_index, result, shard_exit_code):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000770 """Returns wrapped output for swarming task shard."""
771 tag = 'index %s (machine tag: %s, id: %s)' % (
Vadim Shtayurab450c602014-05-12 19:23:25 -0700772 shard_index,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000773 result['machine_id'],
774 result.get('machine_tag', 'unknown'))
775 return (
776 '\n'
777 '================================================================\n'
778 'Begin output from shard %s\n'
779 '================================================================\n'
780 '\n'
781 '%s'
782 '================================================================\n'
Vadim Shtayura473455a2014-05-14 15:22:35 -0700783 'End output from shard %s.\nExit code %d (%s).\n'
784 '================================================================\n') % (
785 tag, result['output'] or NO_OUTPUT_FOUND, tag,
786 shard_exit_code, hex(0xffffffff & shard_exit_code))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000787
788
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700789def collect(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700790 url, task_name, shards, timeout, decorate,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700791 print_status_updates, task_summary_json, task_output_dir):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500792 """Retrieves results of a Swarming task."""
Vadim Shtayurab450c602014-05-12 19:23:25 -0700793 # Grab task keys for each shard. Order is important, used to figure out
794 # shard index based on the key.
795 # TODO(vadimsh): Simplify this once server support is added.
796 task_keys = []
797 for index in xrange(shards):
798 shard_task_name = get_shard_task_name(task_name, shards, index)
799 logging.info('Collecting %s', shard_task_name)
800 shard_task_keys = get_task_keys(url, shard_task_name)
801 if not shard_task_keys:
802 raise Failure('No task keys to get results with: %s' % shard_task_name)
803 if len(shard_task_keys) != 1:
804 raise Failure('Expecting only one shard for a task: %s' % shard_task_name)
805 task_keys.append(shard_task_keys[0])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000806
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700807 # Collect summary JSON and output files (if task_output_dir is not None).
808 output_collector = TaskOutputCollector(
809 task_output_dir, task_name, len(task_keys))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700810
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700811 seen_shards = set()
Vadim Shtayurac524f512014-05-15 09:54:56 -0700812 exit_codes = []
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700813
814 try:
815 for index, output in yield_results(
816 url, task_keys, timeout, None, print_status_updates, output_collector):
817 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700818
819 # Grab first non-zero exit code as an overall shard exit code.
820 shard_exit_code = 0
821 for code in map(int, (output['exit_codes'] or '1').split(',')):
822 if code:
823 shard_exit_code = code
824 break
Vadim Shtayurac524f512014-05-15 09:54:56 -0700825 exit_codes.append(shard_exit_code)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700826
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700827 if decorate:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700828 print decorate_shard_output(index, output, shard_exit_code)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700829 else:
830 print(
831 '%s/%s: %s' % (
832 output['machine_id'],
833 output['machine_tag'],
834 output['exit_codes']))
835 print(''.join(' %s\n' % l for l in output['output'].splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700836 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700837 summary = output_collector.finalize()
838 if task_summary_json:
839 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700840
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700841 if len(seen_shards) != len(task_keys):
842 missing_shards = [x for x in range(len(task_keys)) if x not in seen_shards]
843 print >> sys.stderr, ('Results from some shards are missing: %s' %
844 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700845 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700846
Vadim Shtayurac524f512014-05-15 09:54:56 -0700847 return int(bool(any(exit_codes)))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000848
849
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400850def add_filter_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500851 parser.filter_group = tools.optparse.OptionGroup(parser, 'Filtering slaves')
852 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500853 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500854 dest='dimensions', metavar='FOO bar',
855 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500856 parser.add_option_group(parser.filter_group)
857
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400858
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400859def process_filter_options(parser, options):
860 options.dimensions = dict(options.dimensions)
861 if not options.dimensions:
862 parser.error('Please at least specify one --dimension')
863
864
Vadim Shtayurab450c602014-05-12 19:23:25 -0700865def add_sharding_options(parser):
866 parser.sharding_group = tools.optparse.OptionGroup(parser, 'Sharding options')
867 parser.sharding_group.add_option(
868 '--shards', type='int', default=1,
869 help='Number of shards to trigger and collect.')
870 parser.add_option_group(parser.sharding_group)
871
872
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400873def add_trigger_options(parser):
874 """Adds all options to trigger a task on Swarming."""
875 isolateserver.add_isolate_server_options(parser, True)
876 add_filter_options(parser)
877
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500878 parser.task_group = tools.optparse.OptionGroup(parser, 'Task properties')
879 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500880 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700881 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500882 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500883 '--priority', type='int', default=100,
884 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500885 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500886 '-T', '--task-name',
887 help='Display name of the task. It uniquely identifies the task. '
Vadim Shtayurac3d97b02014-04-26 19:16:05 -0700888 'Defaults to <base_name>/<dimensions>/<isolated hash>/<timestamp> '
889 'if an isolated file is provided, if a hash is provided, it '
890 'defaults to <user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400891 parser.task_group.add_option(
892 '--deadline', type='int', default=6*60*60,
893 help='Seconds to allow the task to be pending for a bot to run before '
894 'this task request expires.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500895 parser.add_option_group(parser.task_group)
Marc-Antoine Ruelcd629732013-12-20 15:00:42 -0500896 # TODO(maruel): This is currently written in a chromium-specific way.
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500897 parser.group_logging.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000898 '--profile', action='store_true',
899 default=bool(os.environ.get('ISOLATE_DEBUG')),
900 help='Have run_isolated.py print profiling info')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000901
902
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500903def process_trigger_options(parser, options, args):
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500904 isolateserver.process_isolate_server_options(parser, options)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500905 if len(args) != 1:
906 parser.error('Must pass one .isolated file or its hash (sha1).')
Marc-Antoine Ruel025e7822014-05-01 11:50:24 -0400907 process_filter_options(parser, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000908
909
910def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500911 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000912 '-t', '--timeout',
913 type='float',
914 default=DEFAULT_SHARD_WAIT_TIME,
915 help='Timeout to wait for result, set to 0 for no timeout; default: '
916 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500917 parser.group_logging.add_option(
918 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700919 parser.group_logging.add_option(
920 '--print-status-updates', action='store_true',
921 help='Print periodic status updates')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700922 parser.task_output_group = tools.optparse.OptionGroup(parser, 'Task output')
923 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700924 '--task-summary-json',
925 metavar='FILE',
926 help='Dump a summary of task results to this file as json. It contains '
927 'only shards statuses as know to server directly. Any output files '
928 'emitted by the task can be collected by using --task-output-dir')
929 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700930 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700931 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700932 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700933 'directory contains per-shard directory with output files produced '
934 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700935 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000936
937
Vadim Shtayuraae8085b2014-05-02 17:13:10 -0700938def extract_isolated_command_extra_args(args):
939 try:
940 index = args.index('--')
941 except ValueError:
942 return (args, [])
943 return (args[:index], args[index+1:])
944
945
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500946@subcommand.usage('task_name')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000947def CMDcollect(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500948 """Retrieves results of a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000949
950 The result can be in multiple part if the execution was sharded. It can
951 potentially have retries.
952 """
953 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700954 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000955 (options, args) = parser.parse_args(args)
956 if not args:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500957 parser.error('Must specify one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000958 elif len(args) > 1:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500959 parser.error('Must specify only one task name.')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000960
961 try:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700962 return collect(
963 options.swarming,
964 args[0],
Vadim Shtayurab450c602014-05-12 19:23:25 -0700965 options.shards,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700966 options.timeout,
967 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700968 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700969 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700970 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400971 except Failure:
972 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000973 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000974
975
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400976def CMDquery(parser, args):
977 """Returns information about the bots connected to the Swarming server."""
978 add_filter_options(parser)
979 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400980 '--dead-only', action='store_true',
981 help='Only print dead bots, useful to reap them and reimage broken bots')
982 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400983 '-k', '--keep-dead', action='store_true',
984 help='Do not filter out dead bots')
985 parser.filter_group.add_option(
986 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400987 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400988 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400989
990 if options.keep_dead and options.dead_only:
991 parser.error('Use only one of --keep-dead and --dead-only')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400992 service = net.get_http_service(options.swarming)
993 data = service.json_request('GET', '/swarming/api/v1/bots')
994 if data is None:
995 print >> sys.stderr, 'Failed to access %s' % options.swarming
996 return 1
997 timeout = datetime.timedelta(seconds=data['machine_death_timeout'])
998 utcnow = datetime.datetime.utcnow()
Marc-Antoine Ruele4bebbc2014-06-04 09:36:14 -0400999 for machine in natsort.natsorted(data['machines'], key=lambda x: x['id']):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001000 last_seen = datetime.datetime.strptime(
1001 machine['last_seen'], '%Y-%m-%d %H:%M:%S')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001002 is_dead = utcnow - last_seen > timeout
1003 if options.dead_only:
1004 if not is_dead:
1005 continue
1006 elif not options.keep_dead and is_dead:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001007 continue
1008
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001009 # If the user requested to filter on dimensions, ensure the bot has all the
1010 # dimensions requested.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001011 dimensions = machine['dimensions']
1012 for key, value in options.dimensions:
1013 if key not in dimensions:
1014 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001015 # A bot can have multiple value for a key, for example,
1016 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1017 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001018 if isinstance(dimensions[key], list):
1019 if value not in dimensions[key]:
1020 break
1021 else:
1022 if value != dimensions[key]:
1023 break
1024 else:
Marc-Antoine Ruele4bebbc2014-06-04 09:36:14 -04001025 print machine['id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001026 if not options.bare:
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001027 print ' %s' % dimensions
1028 return 0
1029
1030
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001031@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001032def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001033 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001034
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001035 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001036 """
1037 add_trigger_options(parser)
1038 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001039 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001040 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001041 options, args = parser.parse_args(args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001042 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001043
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001044 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001045 tasks, task_name = trigger(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001046 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001047 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001048 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001049 file_hash_or_isolated=args[0],
1050 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001051 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001052 shards=options.shards,
1053 dimensions=options.dimensions,
1054 env=dict(options.env),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -04001055 deadline=options.deadline,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001056 verbose=options.verbose,
1057 profile=options.profile,
1058 priority=options.priority)
1059 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001060 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001061 'Failed to trigger %s(%s): %s' %
1062 (options.task_name, args[0], e.args[0]))
1063 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001064 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001065 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001066 return 1
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001067 if task_name != options.task_name:
1068 print('Triggered task: %s' % task_name)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001069 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001070 # TODO(maruel): Use task_ids, it's much more efficient!
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001071 return collect(
1072 options.swarming,
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -05001073 task_name,
Vadim Shtayurab450c602014-05-12 19:23:25 -07001074 options.shards,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001075 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001076 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001077 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001078 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001079 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001080 except Failure:
1081 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001082 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001083
1084
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001085@subcommand.usage("(hash|isolated) [-- extra_args]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001086def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001087 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001088
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001089 Accepts either the hash (sha1) of a .isolated file already uploaded or the
1090 path to an .isolated file to archive, packages it if needed and sends a
1091 Swarming manifest file to the Swarming server.
1092
1093 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001094
1095 Passes all extra arguments provided after '--' as additional command line
1096 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001097 """
1098 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001099 add_sharding_options(parser)
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001100 args, isolated_cmd_args = extract_isolated_command_extra_args(args)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001101 parser.add_option(
1102 '--dump-json',
1103 metavar='FILE',
1104 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001105 options, args = parser.parse_args(args)
1106 process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001107
1108 try:
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001109 tasks, task_name = trigger(
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001110 swarming=options.swarming,
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -05001111 isolate_server=options.isolate_server or options.indir,
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001112 namespace=options.namespace,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001113 file_hash_or_isolated=args[0],
1114 task_name=options.task_name,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001115 extra_args=isolated_cmd_args,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001116 shards=options.shards,
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001117 dimensions=options.dimensions,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -05001118 env=dict(options.env),
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -04001119 deadline=options.deadline,
Marc-Antoine Ruela7049872013-11-05 19:28:35 -05001120 verbose=options.verbose,
1121 profile=options.profile,
1122 priority=options.priority)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001123 if tasks:
1124 if task_name != options.task_name:
1125 print('Triggered task: %s' % task_name)
1126 if options.dump_json:
1127 data = {
1128 'base_task_name': task_name,
1129 'tasks': tasks,
1130 }
1131 tools.write_json(options.dump_json, data, True)
1132 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001133 except Failure:
1134 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001135 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001136
1137
1138class OptionParserSwarming(tools.OptionParserWithLogging):
1139 def __init__(self, **kwargs):
1140 tools.OptionParserWithLogging.__init__(
1141 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001142 self.server_group = tools.optparse.OptionGroup(self, 'Server')
1143 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001144 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001145 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001146 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001147 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001148 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001149
1150 def parse_args(self, *args, **kwargs):
1151 options, args = tools.OptionParserWithLogging.parse_args(
1152 self, *args, **kwargs)
1153 options.swarming = options.swarming.rstrip('/')
1154 if not options.swarming:
1155 self.error('--swarming is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001156 auth.process_auth_options(self, options)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001157 return options, args
1158
1159
1160def main(args):
1161 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001162 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001163
1164
1165if __name__ == '__main__':
1166 fix_encoding.fix_encoding()
1167 tools.disable_buffering()
1168 colorama.init()
1169 sys.exit(main(sys.argv[1:]))