blob: 2b5c9bc9053d76376d010b08dba9a28d8c02f321 [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
maruel77f720b2015-09-15 12:35:22 -07008__version__ = '0.8.2'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import subprocess
17import sys
Vadim Shtayurab19319e2014-04-27 08:50:06 -070018import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000019import time
20import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000021
22from third_party import colorama
23from third_party.depot_tools import fix_encoding
24from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000025
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050026from utils import file_path
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040027from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040028from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000029from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040030from utils import on_error
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
maruel@chromium.org0437a732013-08-27 16:05:52 +000033
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080034import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040035import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000036import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000037
38
39ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050040
41
42class Failure(Exception):
43 """Generic failure."""
44 pass
45
46
47### Isolated file handling.
48
49
maruel77f720b2015-09-15 12:35:22 -070050def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050051 """Archives a .isolated file if needed.
52
53 Returns the file hash to trigger and a bool specifying if it was a file (True)
54 or a hash (False).
55 """
56 if arg.endswith('.isolated'):
maruel77f720b2015-09-15 12:35:22 -070057 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050058 if not file_hash:
59 on_error.report('Archival failure %s' % arg)
60 return None, True
61 return file_hash, True
62 elif isolated_format.is_valid_hash(arg, algo):
63 return arg, False
64 else:
65 on_error.report('Invalid hash %s' % arg)
66 return None, False
67
68
69def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050070 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050071
72 Returns:
maruel77f720b2015-09-15 12:35:22 -070073 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050074 """
75 isolated_cmd_args = []
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050076 if not options.isolated:
77 if '--' in args:
78 index = args.index('--')
79 isolated_cmd_args = args[index+1:]
80 args = args[:index]
81 else:
82 # optparse eats '--' sometimes.
83 isolated_cmd_args = args[1:]
84 args = args[:1]
85 if len(args) != 1:
86 raise ValueError(
87 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
88 'process.')
89 # Old code. To be removed eventually.
90 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070091 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050092 if not options.isolated:
93 raise ValueError('Invalid argument %s' % args[0])
94 elif args:
95 is_file = False
96 if '--' in args:
97 index = args.index('--')
98 isolated_cmd_args = args[index+1:]
99 if index != 0:
100 raise ValueError('Unexpected arguments.')
101 else:
102 # optparse eats '--' sometimes.
103 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500104
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500105 # If a file name was passed, use its base name of the isolated hash.
106 # Otherwise, use user name as an approximation of a task name.
107 if not options.task_name:
108 if is_file:
109 key = os.path.splitext(os.path.basename(args[0]))[0]
110 else:
111 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500112 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500113 key,
114 '_'.join(
115 '%s=%s' % (k, v)
116 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500117 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500118
maruel77f720b2015-09-15 12:35:22 -0700119 inputs_ref = FilesRef(
120 isolated=options.isolated,
121 isolatedserver=options.isolate_server,
122 namespace=options.namespace)
123 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500124
125
126### Triggering.
127
128
maruel77f720b2015-09-15 12:35:22 -0700129# See ../appengine/swarming/swarming_rpcs.py.
130FilesRef = collections.namedtuple(
131 'FilesRef',
132 [
133 'isolated',
134 'isolatedserver',
135 'namespace',
136 ])
137
138
139# See ../appengine/swarming/swarming_rpcs.py.
140TaskProperties = collections.namedtuple(
141 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500142 [
143 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144 'dimensions',
145 'env',
maruel77f720b2015-09-15 12:35:22 -0700146 'execution_timeout_secs',
147 'extra_args',
148 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500149 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700150 'inputs_ref',
151 'io_timeout_secs',
152 ])
153
154
155# See ../appengine/swarming/swarming_rpcs.py.
156NewTaskRequest = collections.namedtuple(
157 'NewTaskRequest',
158 [
159 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500160 'name',
maruel77f720b2015-09-15 12:35:22 -0700161 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500162 'priority',
maruel77f720b2015-09-15 12:35:22 -0700163 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 'tags',
165 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500166 ])
167
168
maruel77f720b2015-09-15 12:35:22 -0700169def namedtuple_to_dict(value):
170 """Recursively converts a namedtuple to a dict."""
171 out = dict(value._asdict())
172 for k, v in out.iteritems():
173 if hasattr(v, '_asdict'):
174 out[k] = namedtuple_to_dict(v)
175 return out
176
177
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500178def task_request_to_raw_request(task_request):
179 """Returns the json dict expected by the Swarming server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700180
181 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500182 """
maruel77f720b2015-09-15 12:35:22 -0700183 out = namedtuple_to_dict(task_request)
184 # Maps are not supported until protobuf v3.
185 out['properties']['dimensions'] = [
186 {'key': k, 'value': v}
187 for k, v in out['properties']['dimensions'].iteritems()
188 ]
189 out['properties']['dimensions'].sort(key=lambda x: x['key'])
190 out['properties']['env'] = [
191 {'key': k, 'value': v}
192 for k, v in out['properties']['env'].iteritems()
193 ]
194 out['properties']['env'].sort(key=lambda x: x['key'])
195 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500196
197
maruel77f720b2015-09-15 12:35:22 -0700198def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199 """Triggers a request on the Swarming server and returns the json data.
200
201 It's the low-level function.
202
203 Returns:
204 {
205 'request': {
206 'created_ts': u'2010-01-02 03:04:05',
207 'name': ..
208 },
209 'task_id': '12300',
210 }
211 """
212 logging.info('Triggering: %s', raw_request['name'])
213
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500214 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700215 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500216 if not result:
217 on_error.report('Failed to trigger task %s' % raw_request['name'])
218 return None
219 return result
220
221
222def setup_googletest(env, shards, index):
223 """Sets googletest specific environment variables."""
224 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700225 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
226 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
227 env = env[:]
228 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
229 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500230 return env
231
232
233def trigger_task_shards(swarming, task_request, shards):
234 """Triggers one or many subtasks of a sharded task.
235
236 Returns:
237 Dict with task details, returned to caller as part of --dump-json output.
238 None in case of failure.
239 """
240 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700241 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700243 req['properties']['env'] = setup_googletest(
244 req['properties']['env'], shards, index)
245 req['name'] += ':%s:%s' % (index, shards)
246 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500247
248 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500249 tasks = {}
250 priority_warning = False
251 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700252 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500253 if not task:
254 break
255 logging.info('Request result: %s', task)
256 if (not priority_warning and
257 task['request']['priority'] != task_request.priority):
258 priority_warning = True
259 print >> sys.stderr, (
260 'Priority was reset to %s' % task['request']['priority'])
261 tasks[request['name']] = {
262 'shard_index': index,
263 'task_id': task['task_id'],
264 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
265 }
266
267 # Some shards weren't triggered. Abort everything.
268 if len(tasks) != len(requests):
269 if tasks:
270 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
271 len(tasks), len(requests))
272 for task_dict in tasks.itervalues():
273 abort_task(swarming, task_dict['task_id'])
274 return None
275
276 return tasks
277
278
279### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000280
281
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700282# How often to print status updates to stdout in 'collect'.
283STATUS_UPDATE_INTERVAL = 15 * 60.
284
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400285
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400286class State(object):
287 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000288
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400289 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
290 values are part of the API so if they change, the API changed.
291
292 It's in fact an enum. Values should be in decreasing order of importance.
293 """
294 RUNNING = 0x10
295 PENDING = 0x20
296 EXPIRED = 0x30
297 TIMED_OUT = 0x40
298 BOT_DIED = 0x50
299 CANCELED = 0x60
300 COMPLETED = 0x70
301
maruel77f720b2015-09-15 12:35:22 -0700302 STATES = (
303 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
304 'COMPLETED')
305 STATES_RUNNING = ('RUNNING', 'PENDING')
306 STATES_NOT_RUNNING = (
307 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
308 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
309 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400310
311 _NAMES = {
312 RUNNING: 'Running',
313 PENDING: 'Pending',
314 EXPIRED: 'Expired',
315 TIMED_OUT: 'Execution timed out',
316 BOT_DIED: 'Bot died',
317 CANCELED: 'User canceled',
318 COMPLETED: 'Completed',
319 }
320
maruel77f720b2015-09-15 12:35:22 -0700321 _ENUMS = {
322 'RUNNING': RUNNING,
323 'PENDING': PENDING,
324 'EXPIRED': EXPIRED,
325 'TIMED_OUT': TIMED_OUT,
326 'BOT_DIED': BOT_DIED,
327 'CANCELED': CANCELED,
328 'COMPLETED': COMPLETED,
329 }
330
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400331 @classmethod
332 def to_string(cls, state):
333 """Returns a user-readable string representing a State."""
334 if state not in cls._NAMES:
335 raise ValueError('Invalid state %s' % state)
336 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000337
maruel77f720b2015-09-15 12:35:22 -0700338 @classmethod
339 def from_enum(cls, state):
340 """Returns int value based on the string."""
341 if state not in cls._ENUMS:
342 raise ValueError('Invalid state %s' % state)
343 return cls._ENUMS[state]
344
maruel@chromium.org0437a732013-08-27 16:05:52 +0000345
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700346class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700347 """Assembles task execution summary (for --task-summary-json output).
348
349 Optionally fetches task outputs from isolate server to local disk (used when
350 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700351
352 This object is shared among multiple threads running 'retrieve_results'
353 function, in particular they call 'process_shard_result' method in parallel.
354 """
355
356 def __init__(self, task_output_dir, task_name, shard_count):
357 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
358
359 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700360 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700361 task_name: name of the swarming task results belong to.
362 shard_count: expected number of task shards.
363 """
364 self.task_output_dir = task_output_dir
365 self.task_name = task_name
366 self.shard_count = shard_count
367
368 self._lock = threading.Lock()
369 self._per_shard_results = {}
370 self._storage = None
371
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700372 if self.task_output_dir and not os.path.isdir(self.task_output_dir):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700373 os.makedirs(self.task_output_dir)
374
Vadim Shtayurab450c602014-05-12 19:23:25 -0700375 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376 """Stores results of a single task shard, fetches output files if necessary.
377
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400378 Modifies |result| in place.
379
maruel77f720b2015-09-15 12:35:22 -0700380 shard_index is 0-based.
381
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 Called concurrently from multiple threads.
383 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700385 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 if shard_index < 0 or shard_index >= self.shard_count:
387 logging.warning(
388 'Shard index %d is outside of expected range: [0; %d]',
389 shard_index, self.shard_count - 1)
390 return
391
maruel77f720b2015-09-15 12:35:22 -0700392 if result.get('outputs_ref'):
393 ref = result['outputs_ref']
394 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
395 ref['isolatedserver'],
396 urllib.urlencode(
397 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400398
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399 # Store result dict of that shard, ignore results we've already seen.
400 with self._lock:
401 if shard_index in self._per_shard_results:
402 logging.warning('Ignoring duplicate shard index %d', shard_index)
403 return
404 self._per_shard_results[shard_index] = result
405
406 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700407 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400408 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700409 result['outputs_ref']['isolatedserver'],
410 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400411 if storage:
412 # Output files are supposed to be small and they are not reused across
413 # tasks. So use MemoryCache for them instead of on-disk cache. Make
414 # files writable, so that calling script can delete them.
415 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700416 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400417 storage,
418 isolateserver.MemoryCache(file_mode_mask=0700),
419 os.path.join(self.task_output_dir, str(shard_index)),
420 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700421
422 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700423 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424 with self._lock:
425 # Write an array of shard results with None for missing shards.
426 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427 'shards': [
428 self._per_shard_results.get(i) for i in xrange(self.shard_count)
429 ],
430 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700431 # Write summary.json to task_output_dir as well.
432 if self.task_output_dir:
433 tools.write_json(
434 os.path.join(self.task_output_dir, 'summary.json'),
435 summary,
436 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700437 if self._storage:
438 self._storage.close()
439 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441
442 def _get_storage(self, isolate_server, namespace):
443 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700444 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 with self._lock:
446 if not self._storage:
447 self._storage = isolateserver.get_storage(isolate_server, namespace)
448 else:
449 # Shards must all use exact same isolate server and namespace.
450 if self._storage.location != isolate_server:
451 logging.error(
452 'Task shards are using multiple isolate servers: %s and %s',
453 self._storage.location, isolate_server)
454 return None
455 if self._storage.namespace != namespace:
456 logging.error(
457 'Task shards are using multiple namespaces: %s and %s',
458 self._storage.namespace, namespace)
459 return None
460 return self._storage
461
462
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500463def now():
464 """Exists so it can be mocked easily."""
465 return time.time()
466
467
maruel77f720b2015-09-15 12:35:22 -0700468def parse_time(value):
469 """Converts serialized time from the API to datetime.datetime."""
470 # When microseconds are 0, the '.123456' suffix is elided. This means the
471 # serialized format is not consistent, which confuses the hell out of python.
472 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
473 try:
474 return datetime.datetime.strptime(value, fmt)
475 except ValueError:
476 pass
477 raise ValueError('Failed to parse %s' % value)
478
479
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700480def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400481 base_url, shard_index, task_id, timeout, should_stop, output_collector):
482 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700483
Vadim Shtayurab450c602014-05-12 19:23:25 -0700484 Returns:
485 <result dict> on success.
486 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700487 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000488 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700489 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
490 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700491 started = now()
492 deadline = started + timeout if timeout else None
493 attempt = 0
494
495 while not should_stop.is_set():
496 attempt += 1
497
498 # Waiting for too long -> give up.
499 current_time = now()
500 if deadline and current_time >= deadline:
501 logging.error('retrieve_results(%s) timed out on attempt %d',
502 base_url, attempt)
503 return None
504
505 # Do not spin too fast. Spin faster at the beginning though.
506 # Start with 1 sec delay and for each 30 sec of waiting add another second
507 # of delay, until hitting 15 sec ceiling.
508 if attempt > 1:
509 max_delay = min(15, 1 + (current_time - started) / 30.0)
510 delay = min(max_delay, deadline - current_time) if deadline else max_delay
511 if delay > 0:
512 logging.debug('Waiting %.1f sec before retrying', delay)
513 should_stop.wait(delay)
514 if should_stop.is_set():
515 return None
516
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400517 # Disable internal retries in net.url_read_json, since we are doing retries
518 # ourselves.
519 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
520 result = net.url_read_json(result_url, retry_50x=False)
521 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400522 continue
maruel77f720b2015-09-15 12:35:22 -0700523
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400524 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700525 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400526 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700527 result['output'] = out.get('output') if out else out
528 if not result['output']:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400529 logging.error('No output found for task %s', task_id)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700530 # Record the result, try to fetch attached output files (if any).
531 if output_collector:
532 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700533 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700534 if result.get('internal_failure'):
535 logging.error('Internal error!')
536 elif result['state'] == 'BOT_DIED':
537 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700538 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000539
540
maruel77f720b2015-09-15 12:35:22 -0700541def convert_to_old_format(result):
542 """Converts the task result data from Endpoints API format to old API format
543 for compatibility.
544
545 This goes into the file generated as --task-summary-json.
546 """
547 # Sets default.
548 result.setdefault('abandoned_ts', None)
549 result.setdefault('bot_id', None)
550 result.setdefault('bot_version', None)
551 result.setdefault('children_task_ids', [])
552 result.setdefault('completed_ts', None)
553 result.setdefault('cost_saved_usd', None)
554 result.setdefault('costs_usd', None)
555 result.setdefault('deduped_from', None)
556 result.setdefault('name', None)
557 result.setdefault('outputs_ref', None)
558 result.setdefault('properties_hash', None)
559 result.setdefault('server_versions', None)
560 result.setdefault('started_ts', None)
561 result.setdefault('tags', None)
562 result.setdefault('user', None)
563
564 # Convertion back to old API.
565 duration = result.pop('duration', None)
566 result['durations'] = [duration] if duration else []
567 exit_code = result.pop('exit_code', None)
568 result['exit_codes'] = [int(exit_code)] if exit_code else []
569 result['id'] = result.pop('task_id')
570 result['isolated_out'] = result.get('outputs_ref', None)
571 output = result.pop('output', None)
572 result['outputs'] = [output] if output else []
573 # properties_hash
574 # server_version
575 # Endpoints result 'state' as string. For compatibility with old code, convert
576 # to int.
577 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700578 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700579 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700580 if 'bot_dimensions' in result:
581 result['bot_dimensions'] = {
582 i['key']: i['value'] for i in result['bot_dimensions']
583 }
584 else:
585 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700586
587
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700588def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400589 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
590 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500591 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000592
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700593 Duplicate shards are ignored. Shards are yielded in order of completion.
594 Timed out shards are NOT yielded at all. Caller can compare number of yielded
595 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000596
597 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500598 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 +0000599 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500600
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700601 output_collector is an optional instance of TaskOutputCollector that will be
602 used to fetch files produced by a task from isolate server to the local disk.
603
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500604 Yields:
605 (index, result). In particular, 'result' is defined as the
606 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000607 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000608 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400609 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700610 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700611 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700612
maruel@chromium.org0437a732013-08-27 16:05:52 +0000613 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
614 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700615 # Adds a task to the thread pool to call 'retrieve_results' and return
616 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400617 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700618 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000619 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400620 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
621 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700622
623 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400624 for shard_index, task_id in enumerate(task_ids):
625 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700626
627 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400628 shards_remaining = range(len(task_ids))
629 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700630 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700631 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700632 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 shard_index, result = results_channel.pull(
634 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700635 except threading_utils.TaskChannel.Timeout:
636 if print_status_updates:
637 print(
638 'Waiting for results from the following shards: %s' %
639 ', '.join(map(str, shards_remaining)))
640 sys.stdout.flush()
641 continue
642 except Exception:
643 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700644
645 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700646 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000647 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500648 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000649 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700650
Vadim Shtayurab450c602014-05-12 19:23:25 -0700651 # Yield back results to the caller.
652 assert shard_index in shards_remaining
653 shards_remaining.remove(shard_index)
654 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700655
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700657 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000658 should_stop.set()
659
660
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400661def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000662 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700663 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400664 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700665 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
666 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400667 else:
668 pending = 'N/A'
669
maruel77f720b2015-09-15 12:35:22 -0700670 if metadata.get('duration') is not None:
671 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400672 else:
673 duration = 'N/A'
674
maruel77f720b2015-09-15 12:35:22 -0700675 if metadata.get('exit_code') is not None:
676 # Integers are encoded as string to not loose precision.
677 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400678 else:
679 exit_code = 'N/A'
680
681 bot_id = metadata.get('bot_id') or 'N/A'
682
maruel77f720b2015-09-15 12:35:22 -0700683 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400684 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400685 tag_footer = (
686 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
687 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400688
689 tag_len = max(len(tag_header), len(tag_footer))
690 dash_pad = '+-%s-+\n' % ('-' * tag_len)
691 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
692 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
693
694 header = dash_pad + tag_header + dash_pad
695 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700696 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400697 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000698
699
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700700def collect(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400701 swarming, task_name, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400702 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700703 """Retrieves results of a Swarming task.
704
705 Returns:
706 process exit code that should be returned to the user.
707 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700708 # Collect summary JSON and output files (if task_output_dir is not None).
709 output_collector = TaskOutputCollector(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400710 task_output_dir, task_name, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700711
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700712 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700713 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400714 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700715 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400716 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400717 swarming, task_ids, timeout, None, print_status_updates,
718 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700719 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700720
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400721 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700722 shard_exit_code = metadata.get('exit_code')
723 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700724 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700725 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700726 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400727 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700728 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700729
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700730 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400731 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400732 if len(seen_shards) < len(task_ids):
733 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700734 else:
maruel77f720b2015-09-15 12:35:22 -0700735 print('%s: %s %s' % (
736 metadata.get('bot_id', 'N/A'),
737 metadata['task_id'],
738 shard_exit_code))
739 if metadata['output']:
740 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400741 if output:
742 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700743 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700744 summary = output_collector.finalize()
745 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700746 # TODO(maruel): Make this optional.
747 for i in summary['shards']:
748 if i:
749 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700750 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700751
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400752 if decorate and total_duration:
753 print('Total duration: %.1fs' % total_duration)
754
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400755 if len(seen_shards) != len(task_ids):
756 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700757 print >> sys.stderr, ('Results from some shards are missing: %s' %
758 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700759 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700760
maruela5490782015-09-30 10:56:59 -0700761 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000762
763
maruel77f720b2015-09-15 12:35:22 -0700764### API management.
765
766
767class APIError(Exception):
768 pass
769
770
771def endpoints_api_discovery_apis(host):
772 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
773 the APIs exposed by a host.
774
775 https://developers.google.com/discovery/v1/reference/apis/list
776 """
777 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
778 if data is None:
779 raise APIError('Failed to discover APIs on %s' % host)
780 out = {}
781 for api in data['items']:
782 if api['id'] == 'discovery:v1':
783 continue
784 # URL is of the following form:
785 # url = host + (
786 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
787 api_data = net.url_read_json(api['discoveryRestUrl'])
788 if api_data is None:
789 raise APIError('Failed to discover %s on %s' % (api['id'], host))
790 out[api['id']] = api_data
791 return out
792
793
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500794### Commands.
795
796
797def abort_task(_swarming, _manifest):
798 """Given a task manifest that was triggered, aborts its execution."""
799 # TODO(vadimsh): No supported by the server yet.
800
801
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400802def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400803 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500804 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500805 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500806 dest='dimensions', metavar='FOO bar',
807 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500808 parser.add_option_group(parser.filter_group)
809
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400810
Vadim Shtayurab450c602014-05-12 19:23:25 -0700811def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400812 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700813 parser.sharding_group.add_option(
814 '--shards', type='int', default=1,
815 help='Number of shards to trigger and collect.')
816 parser.add_option_group(parser.sharding_group)
817
818
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400819def add_trigger_options(parser):
820 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500821 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400822 add_filter_options(parser)
823
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400824 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500825 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500826 '-s', '--isolated',
827 help='Hash of the .isolated to grab from the isolate server')
828 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500829 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700830 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500831 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500832 '--priority', type='int', default=100,
833 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500834 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500835 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400836 help='Display name of the task. Defaults to '
837 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
838 'isolated file is provided, if a hash is provided, it defaults to '
839 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400840 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400841 '--tags', action='append', default=[],
842 help='Tags to assign to the task.')
843 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500844 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400845 help='User associated with the task. Defaults to authenticated user on '
846 'the server.')
847 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400848 '--idempotent', action='store_true', default=False,
849 help='When set, the server will actively try to find a previous task '
850 'with the same parameter and return this result instead if possible')
851 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400852 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400853 help='Seconds to allow the task to be pending for a bot to run before '
854 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400855 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400856 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400857 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400858 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400859 '--hard-timeout', type='int', default=60*60,
860 help='Seconds to allow the task to complete.')
861 parser.task_group.add_option(
862 '--io-timeout', type='int', default=20*60,
863 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500864 parser.task_group.add_option(
865 '--raw-cmd', action='store_true', default=False,
866 help='When set, the command after -- is used as-is without run_isolated. '
867 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500868 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000869
870
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500871def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500872 """Processes trigger options and uploads files to isolate server if necessary.
873 """
874 options.dimensions = dict(options.dimensions)
875 options.env = dict(options.env)
876
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500877 if not options.dimensions:
878 parser.error('Please at least specify one --dimension')
879 if options.raw_cmd:
880 if not args:
881 parser.error(
882 'Arguments with --raw-cmd should be passed after -- as command '
883 'delimiter.')
884 if options.isolate_server:
885 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
886
887 command = args
888 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500889 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500890 options.user,
891 '_'.join(
892 '%s=%s' % (k, v)
893 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700894 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500895 else:
896 isolateserver.process_isolate_server_options(parser, options, False)
897 try:
maruel77f720b2015-09-15 12:35:22 -0700898 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500899 except ValueError as e:
900 parser.error(str(e))
901
maruel77f720b2015-09-15 12:35:22 -0700902 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
903 # actual command to run.
904 properties = TaskProperties(
905 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500906 dimensions=options.dimensions,
907 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700908 execution_timeout_secs=options.hard_timeout,
909 extra_args=command if inputs_ref else None,
910 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500911 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700912 inputs_ref=inputs_ref,
913 io_timeout_secs=options.io_timeout)
914 return NewTaskRequest(
915 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500916 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700917 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500918 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700919 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500920 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700921 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000922
923
924def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500925 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000926 '-t', '--timeout',
927 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400928 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000929 help='Timeout to wait for result, set to 0 for no timeout; default: '
930 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500931 parser.group_logging.add_option(
932 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700933 parser.group_logging.add_option(
934 '--print-status-updates', action='store_true',
935 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400936 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700937 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700938 '--task-summary-json',
939 metavar='FILE',
940 help='Dump a summary of task results to this file as json. It contains '
941 'only shards statuses as know to server directly. Any output files '
942 'emitted by the task can be collected by using --task-output-dir')
943 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700944 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700945 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700946 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700947 'directory contains per-shard directory with output files produced '
948 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700949 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000950
951
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400952@subcommand.usage('bots...')
953def CMDbot_delete(parser, args):
954 """Forcibly deletes bots from the Swarming server."""
955 parser.add_option(
956 '-f', '--force', action='store_true',
957 help='Do not prompt for confirmation')
958 options, args = parser.parse_args(args)
959 if not args:
960 parser.error('Please specific bots to delete')
961
962 bots = sorted(args)
963 if not options.force:
964 print('Delete the following bots?')
965 for bot in bots:
966 print(' %s' % bot)
967 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
968 print('Goodbye.')
969 return 1
970
971 result = 0
972 for bot in bots:
maruel77f720b2015-09-15 12:35:22 -0700973 url = '%s/_ah/api/swarming/v1/bot/%s' % (options.swarming, bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400974 if net.url_read_json(url, method='DELETE') is None:
975 print('Deleting %s failed' % bot)
976 result = 1
977 return result
978
979
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400980def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400981 """Returns information about the bots connected to the Swarming server."""
982 add_filter_options(parser)
983 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400984 '--dead-only', action='store_true',
985 help='Only print dead bots, useful to reap them and reimage broken bots')
986 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400987 '-k', '--keep-dead', action='store_true',
988 help='Do not filter out dead bots')
989 parser.filter_group.add_option(
990 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400991 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400992 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400993
994 if options.keep_dead and options.dead_only:
995 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700996
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400997 bots = []
998 cursor = None
999 limit = 250
1000 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001001 base_url = (
1002 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001003 while True:
1004 url = base_url
1005 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001006 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001007 data = net.url_read_json(url)
1008 if data is None:
1009 print >> sys.stderr, 'Failed to access %s' % options.swarming
1010 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001011 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001012 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001013 if not cursor:
1014 break
1015
maruel77f720b2015-09-15 12:35:22 -07001016 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001017 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001018 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001019 continue
maruel77f720b2015-09-15 12:35:22 -07001020 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001021 continue
1022
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001023 # If the user requested to filter on dimensions, ensure the bot has all the
1024 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001025 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001026 for key, value in options.dimensions:
1027 if key not in dimensions:
1028 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001029 # A bot can have multiple value for a key, for example,
1030 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1031 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001032 if isinstance(dimensions[key], list):
1033 if value not in dimensions[key]:
1034 break
1035 else:
1036 if value != dimensions[key]:
1037 break
1038 else:
maruel77f720b2015-09-15 12:35:22 -07001039 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001040 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001041 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001042 if bot.get('task_id'):
1043 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001044 return 0
1045
1046
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001047@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001048def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001049 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001050
1051 The result can be in multiple part if the execution was sharded. It can
1052 potentially have retries.
1053 """
1054 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001055 parser.add_option(
1056 '-j', '--json',
1057 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001058 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001059 if not args and not options.json:
1060 parser.error('Must specify at least one task id or --json.')
1061 if args and options.json:
1062 parser.error('Only use one of task id or --json.')
1063
1064 if options.json:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001065 try:
1066 with open(options.json) as f:
1067 tasks = sorted(
1068 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1069 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001070 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001071 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001072 else:
1073 valid = frozenset('0123456789abcdef')
1074 if any(not valid.issuperset(task_id) for task_id in args):
1075 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001076
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001077 try:
1078 return collect(
1079 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001080 None,
1081 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001082 options.timeout,
1083 options.decorate,
1084 options.print_status_updates,
1085 options.task_summary_json,
1086 options.task_output_dir)
1087 except Failure:
1088 on_error.report(None)
1089 return 1
1090
1091
maruelbea00862015-09-18 09:55:36 -07001092@subcommand.usage('[filename]')
1093def CMDput_bootstrap(parser, args):
1094 """Uploads a new version of bootstrap.py."""
1095 options, args = parser.parse_args(args)
1096 if len(args) != 1:
1097 parser.error('Must specify file to upload')
1098 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
1099 with open(args[0], 'rb') as f:
1100 content = f.read().decode('utf-8')
1101 data = net.url_read_json(url, data={'content': content})
1102 print data
1103 return 0
1104
1105
1106@subcommand.usage('[filename]')
1107def CMDput_bot_config(parser, args):
1108 """Uploads a new version of bot_config.py."""
1109 options, args = parser.parse_args(args)
1110 if len(args) != 1:
1111 parser.error('Must specify file to upload')
1112 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
1113 with open(args[0], 'rb') as f:
1114 content = f.read().decode('utf-8')
1115 data = net.url_read_json(url, data={'content': content})
1116 print data
1117 return 0
1118
1119
maruel77f720b2015-09-15 12:35:22 -07001120@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001121def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001122 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1123 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001124
1125 Examples:
maruel77f720b2015-09-15 12:35:22 -07001126 Listing all bots:
1127 swarming.py query -S https://server-url bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001128
maruel77f720b2015-09-15 12:35:22 -07001129 Listing last 10 tasks on a specific bot named 'swarm1':
1130 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001131 """
1132 CHUNK_SIZE = 250
1133
1134 parser.add_option(
1135 '-L', '--limit', type='int', default=200,
1136 help='Limit to enforce on limitless items (like number of tasks); '
1137 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001138 parser.add_option(
1139 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001140 parser.add_option(
1141 '--progress', action='store_true',
1142 help='Prints a dot at each request to show progress')
1143 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001144 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001145 parser.error(
1146 'Must specify only method name and optionally query args properly '
1147 'escaped.')
1148 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001149 url = base_url
1150 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001151 # Check check, change if not working out.
1152 merge_char = '&' if '?' in url else '?'
1153 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001154 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001155 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001156 # TODO(maruel): Do basic diagnostic.
1157 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001158 return 1
1159
1160 # Some items support cursors. Try to get automatically if cursors are needed
1161 # by looking at the 'cursor' items.
1162 while (
1163 data.get('cursor') and
1164 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001165 merge_char = '&' if '?' in base_url else '?'
1166 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001167 if options.limit:
1168 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001169 if options.progress:
1170 sys.stdout.write('.')
1171 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001172 new = net.url_read_json(url)
1173 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001174 if options.progress:
1175 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001176 print >> sys.stderr, 'Failed to access %s' % options.swarming
1177 return 1
1178 data['items'].extend(new['items'])
maruel77f720b2015-09-15 12:35:22 -07001179 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001180
maruel77f720b2015-09-15 12:35:22 -07001181 if options.progress:
1182 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001183 if options.limit and len(data.get('items', [])) > options.limit:
1184 data['items'] = data['items'][:options.limit]
1185 data.pop('cursor', None)
1186
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001187 if options.json:
maruel77f720b2015-09-15 12:35:22 -07001188 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001189 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001190 try:
maruel77f720b2015-09-15 12:35:22 -07001191 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001192 sys.stdout.write('\n')
1193 except IOError:
1194 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001195 return 0
1196
1197
maruel77f720b2015-09-15 12:35:22 -07001198def CMDquery_list(parser, args):
1199 """Returns list of all the Swarming APIs that can be used with command
1200 'query'.
1201 """
1202 parser.add_option(
1203 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1204 options, args = parser.parse_args(args)
1205 if args:
1206 parser.error('No argument allowed.')
1207
1208 try:
1209 apis = endpoints_api_discovery_apis(options.swarming)
1210 except APIError as e:
1211 parser.error(str(e))
1212 if options.json:
1213 with open(options.json, 'wb') as f:
1214 json.dump(apis, f)
1215 else:
1216 help_url = (
1217 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1218 options.swarming)
1219 for api_id, api in sorted(apis.iteritems()):
1220 print api_id
1221 print ' ' + api['description']
1222 for resource_name, resource in sorted(api['resources'].iteritems()):
1223 print ''
1224 for method_name, method in sorted(resource['methods'].iteritems()):
1225 # Only list the GET ones.
1226 if method['httpMethod'] != 'GET':
1227 continue
1228 print '- %s.%s: %s' % (
1229 resource_name, method_name, method['path'])
1230 print ' ' + method['description']
1231 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1232 return 0
1233
1234
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001235@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001236def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001237 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001238
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001239 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001240 """
1241 add_trigger_options(parser)
1242 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001243 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001244 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001245 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001246 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001247 tasks = trigger_task_shards(
1248 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001249 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001250 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001251 'Failed to trigger %s(%s): %s' %
1252 (options.task_name, args[0], e.args[0]))
1253 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001254 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001255 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001256 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001257 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001258 task_ids = [
1259 t['task_id']
1260 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1261 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001262 try:
1263 return collect(
1264 options.swarming,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001265 options.task_name,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001266 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001267 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001268 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001269 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001270 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001271 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001272 except Failure:
1273 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001274 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001275
1276
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001277@subcommand.usage('task_id')
1278def CMDreproduce(parser, args):
1279 """Runs a task locally that was triggered on the server.
1280
1281 This running locally the same commands that have been run on the bot. The data
1282 downloaded will be in a subdirectory named 'work' of the current working
1283 directory.
1284 """
1285 options, args = parser.parse_args(args)
1286 if len(args) != 1:
1287 parser.error('Must specify exactly one task id.')
1288
maruel77f720b2015-09-15 12:35:22 -07001289 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001290 request = net.url_read_json(url)
1291 if not request:
1292 print >> sys.stderr, 'Failed to retrieve request data for the task'
1293 return 1
1294
1295 if not os.path.isdir('work'):
1296 os.mkdir('work')
1297
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001298 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001299 env = None
1300 if properties['env']:
1301 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001302 logging.info('env: %r', properties['env'])
1303 env.update(
maruel77f720b2015-09-15 12:35:22 -07001304 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1305 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001306
maruel77f720b2015-09-15 12:35:22 -07001307 try:
1308 return subprocess.call(properties['command'], env=env, cwd='work')
1309 except OSError as e:
1310 print >> sys.stderr, 'Failed to run: %s' % ' '.join(properties['command'])
1311 print >> sys.stderr, str(e)
1312 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001313
1314
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001315@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001316def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001317 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001318
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001319 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001320 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001321
1322 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001323
1324 Passes all extra arguments provided after '--' as additional command line
1325 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001326 """
1327 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001328 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001329 parser.add_option(
1330 '--dump-json',
1331 metavar='FILE',
1332 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001333 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001334 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001335 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001336 tasks = trigger_task_shards(
1337 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001338 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001339 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001340 tasks_sorted = sorted(
1341 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001342 if options.dump_json:
1343 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001344 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001345 'tasks': tasks,
1346 }
1347 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001348 print('To collect results, use:')
1349 print(' swarming.py collect -S %s --json %s' %
1350 (options.swarming, options.dump_json))
1351 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001352 print('To collect results, use:')
1353 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001354 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1355 print('Or visit:')
1356 for t in tasks_sorted:
1357 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001358 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001359 except Failure:
1360 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001361 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001362
1363
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001364class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001365 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001366 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001367 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001368 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001369 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001370 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001371 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001372 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001373 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001374 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001375
1376 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001377 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001378 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001379 auth.process_auth_options(self, options)
1380 user = self._process_swarming(options)
1381 if hasattr(options, 'user') and not options.user:
1382 options.user = user
1383 return options, args
1384
1385 def _process_swarming(self, options):
1386 """Processes the --swarming option and aborts if not specified.
1387
1388 Returns the identity as determined by the server.
1389 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001390 if not options.swarming:
1391 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001392 try:
1393 options.swarming = net.fix_url(options.swarming)
1394 except ValueError as e:
1395 self.error('--swarming %s' % e)
1396 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001397 try:
1398 user = auth.ensure_logged_in(options.swarming)
1399 except ValueError as e:
1400 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001401 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001402
1403
1404def main(args):
1405 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001406 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001407
1408
1409if __name__ == '__main__':
1410 fix_encoding.fix_encoding()
1411 tools.disable_buffering()
1412 colorama.init()
1413 sys.exit(main(sys.argv[1:]))