blob: bb9e534ae68fde1b3a3348edd1f866bf6a0bc238 [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
maruel29ab2fd2015-10-16 11:44:01 -07008__version__ = '0.8.3'
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
maruel29ab2fd2015-10-16 11:44:01 -070018import tempfile
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel@chromium.org0437a732013-08-27 16:05:52 +000033from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000034from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000035
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040037import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000038import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000039
40
41ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050042
43
44class Failure(Exception):
45 """Generic failure."""
46 pass
47
48
49### Isolated file handling.
50
51
maruel77f720b2015-09-15 12:35:22 -070052def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050053 """Archives a .isolated file if needed.
54
55 Returns the file hash to trigger and a bool specifying if it was a file (True)
56 or a hash (False).
57 """
58 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070059 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070060 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050061 if not file_hash:
62 on_error.report('Archival failure %s' % arg)
63 return None, True
64 return file_hash, True
65 elif isolated_format.is_valid_hash(arg, algo):
66 return arg, False
67 else:
68 on_error.report('Invalid hash %s' % arg)
69 return None, False
70
71
72def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050073 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050074
75 Returns:
maruel77f720b2015-09-15 12:35:22 -070076 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050077 """
78 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070079 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050080 if not options.isolated:
81 if '--' in args:
82 index = args.index('--')
83 isolated_cmd_args = args[index+1:]
84 args = args[:index]
85 else:
86 # optparse eats '--' sometimes.
87 isolated_cmd_args = args[1:]
88 args = args[:1]
89 if len(args) != 1:
90 raise ValueError(
91 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
92 'process.')
93 # Old code. To be removed eventually.
94 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070095 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050096 if not options.isolated:
97 raise ValueError('Invalid argument %s' % args[0])
98 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050099 if '--' in args:
100 index = args.index('--')
101 isolated_cmd_args = args[index+1:]
102 if index != 0:
103 raise ValueError('Unexpected arguments.')
104 else:
105 # optparse eats '--' sometimes.
106 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500107
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500108 # If a file name was passed, use its base name of the isolated hash.
109 # Otherwise, use user name as an approximation of a task name.
110 if not options.task_name:
111 if is_file:
112 key = os.path.splitext(os.path.basename(args[0]))[0]
113 else:
114 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500115 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500116 key,
117 '_'.join(
118 '%s=%s' % (k, v)
119 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500120 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500121
maruel77f720b2015-09-15 12:35:22 -0700122 inputs_ref = FilesRef(
123 isolated=options.isolated,
124 isolatedserver=options.isolate_server,
125 namespace=options.namespace)
126 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500127
128
129### Triggering.
130
131
maruel77f720b2015-09-15 12:35:22 -0700132# See ../appengine/swarming/swarming_rpcs.py.
133FilesRef = collections.namedtuple(
134 'FilesRef',
135 [
136 'isolated',
137 'isolatedserver',
138 'namespace',
139 ])
140
141
142# See ../appengine/swarming/swarming_rpcs.py.
143TaskProperties = collections.namedtuple(
144 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500145 [
146 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500147 'dimensions',
148 'env',
maruel77f720b2015-09-15 12:35:22 -0700149 'execution_timeout_secs',
150 'extra_args',
151 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500152 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700153 'inputs_ref',
154 'io_timeout_secs',
155 ])
156
157
158# See ../appengine/swarming/swarming_rpcs.py.
159NewTaskRequest = collections.namedtuple(
160 'NewTaskRequest',
161 [
162 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500163 'name',
maruel77f720b2015-09-15 12:35:22 -0700164 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500165 'priority',
maruel77f720b2015-09-15 12:35:22 -0700166 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500167 'tags',
168 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500169 ])
170
171
maruel77f720b2015-09-15 12:35:22 -0700172def namedtuple_to_dict(value):
173 """Recursively converts a namedtuple to a dict."""
174 out = dict(value._asdict())
175 for k, v in out.iteritems():
176 if hasattr(v, '_asdict'):
177 out[k] = namedtuple_to_dict(v)
178 return out
179
180
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500181def task_request_to_raw_request(task_request):
182 """Returns the json dict expected by the Swarming server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700183
184 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500185 """
maruel77f720b2015-09-15 12:35:22 -0700186 out = namedtuple_to_dict(task_request)
187 # Maps are not supported until protobuf v3.
188 out['properties']['dimensions'] = [
189 {'key': k, 'value': v}
190 for k, v in out['properties']['dimensions'].iteritems()
191 ]
192 out['properties']['dimensions'].sort(key=lambda x: x['key'])
193 out['properties']['env'] = [
194 {'key': k, 'value': v}
195 for k, v in out['properties']['env'].iteritems()
196 ]
197 out['properties']['env'].sort(key=lambda x: x['key'])
198 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199
200
maruel77f720b2015-09-15 12:35:22 -0700201def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500202 """Triggers a request on the Swarming server and returns the json data.
203
204 It's the low-level function.
205
206 Returns:
207 {
208 'request': {
209 'created_ts': u'2010-01-02 03:04:05',
210 'name': ..
211 },
212 'task_id': '12300',
213 }
214 """
215 logging.info('Triggering: %s', raw_request['name'])
216
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500217 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700218 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500219 if not result:
220 on_error.report('Failed to trigger task %s' % raw_request['name'])
221 return None
222 return result
223
224
225def setup_googletest(env, shards, index):
226 """Sets googletest specific environment variables."""
227 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700228 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
229 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
230 env = env[:]
231 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
232 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500233 return env
234
235
236def trigger_task_shards(swarming, task_request, shards):
237 """Triggers one or many subtasks of a sharded task.
238
239 Returns:
240 Dict with task details, returned to caller as part of --dump-json output.
241 None in case of failure.
242 """
243 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700244 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500245 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700246 req['properties']['env'] = setup_googletest(
247 req['properties']['env'], shards, index)
248 req['name'] += ':%s:%s' % (index, shards)
249 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250
251 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500252 tasks = {}
253 priority_warning = False
254 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700255 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500256 if not task:
257 break
258 logging.info('Request result: %s', task)
259 if (not priority_warning and
260 task['request']['priority'] != task_request.priority):
261 priority_warning = True
262 print >> sys.stderr, (
263 'Priority was reset to %s' % task['request']['priority'])
264 tasks[request['name']] = {
265 'shard_index': index,
266 'task_id': task['task_id'],
267 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
268 }
269
270 # Some shards weren't triggered. Abort everything.
271 if len(tasks) != len(requests):
272 if tasks:
273 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
274 len(tasks), len(requests))
275 for task_dict in tasks.itervalues():
276 abort_task(swarming, task_dict['task_id'])
277 return None
278
279 return tasks
280
281
282### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000283
284
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700285# How often to print status updates to stdout in 'collect'.
286STATUS_UPDATE_INTERVAL = 15 * 60.
287
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400288
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400289class State(object):
290 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000291
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400292 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
293 values are part of the API so if they change, the API changed.
294
295 It's in fact an enum. Values should be in decreasing order of importance.
296 """
297 RUNNING = 0x10
298 PENDING = 0x20
299 EXPIRED = 0x30
300 TIMED_OUT = 0x40
301 BOT_DIED = 0x50
302 CANCELED = 0x60
303 COMPLETED = 0x70
304
maruel77f720b2015-09-15 12:35:22 -0700305 STATES = (
306 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
307 'COMPLETED')
308 STATES_RUNNING = ('RUNNING', 'PENDING')
309 STATES_NOT_RUNNING = (
310 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
311 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
312 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400313
314 _NAMES = {
315 RUNNING: 'Running',
316 PENDING: 'Pending',
317 EXPIRED: 'Expired',
318 TIMED_OUT: 'Execution timed out',
319 BOT_DIED: 'Bot died',
320 CANCELED: 'User canceled',
321 COMPLETED: 'Completed',
322 }
323
maruel77f720b2015-09-15 12:35:22 -0700324 _ENUMS = {
325 'RUNNING': RUNNING,
326 'PENDING': PENDING,
327 'EXPIRED': EXPIRED,
328 'TIMED_OUT': TIMED_OUT,
329 'BOT_DIED': BOT_DIED,
330 'CANCELED': CANCELED,
331 'COMPLETED': COMPLETED,
332 }
333
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400334 @classmethod
335 def to_string(cls, state):
336 """Returns a user-readable string representing a State."""
337 if state not in cls._NAMES:
338 raise ValueError('Invalid state %s' % state)
339 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000340
maruel77f720b2015-09-15 12:35:22 -0700341 @classmethod
342 def from_enum(cls, state):
343 """Returns int value based on the string."""
344 if state not in cls._ENUMS:
345 raise ValueError('Invalid state %s' % state)
346 return cls._ENUMS[state]
347
maruel@chromium.org0437a732013-08-27 16:05:52 +0000348
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700349class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700350 """Assembles task execution summary (for --task-summary-json output).
351
352 Optionally fetches task outputs from isolate server to local disk (used when
353 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700354
355 This object is shared among multiple threads running 'retrieve_results'
356 function, in particular they call 'process_shard_result' method in parallel.
357 """
358
maruel0eb1d1b2015-10-02 14:48:21 -0700359 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
361
362 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700363 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364 shard_count: expected number of task shards.
365 """
maruel12e30012015-10-09 11:55:35 -0700366 self.task_output_dir = (
367 unicode(os.path.abspath(task_output_dir))
368 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369 self.shard_count = shard_count
370
371 self._lock = threading.Lock()
372 self._per_shard_results = {}
373 self._storage = None
374
maruel12e30012015-10-09 11:55:35 -0700375 if self.task_output_dir and not fs.isdir(self.task_output_dir):
376 fs.makedirs(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377
Vadim Shtayurab450c602014-05-12 19:23:25 -0700378 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700379 """Stores results of a single task shard, fetches output files if necessary.
380
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400381 Modifies |result| in place.
382
maruel77f720b2015-09-15 12:35:22 -0700383 shard_index is 0-based.
384
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 Called concurrently from multiple threads.
386 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700388 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700389 if shard_index < 0 or shard_index >= self.shard_count:
390 logging.warning(
391 'Shard index %d is outside of expected range: [0; %d]',
392 shard_index, self.shard_count - 1)
393 return
394
maruel77f720b2015-09-15 12:35:22 -0700395 if result.get('outputs_ref'):
396 ref = result['outputs_ref']
397 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
398 ref['isolatedserver'],
399 urllib.urlencode(
400 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400401
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 # Store result dict of that shard, ignore results we've already seen.
403 with self._lock:
404 if shard_index in self._per_shard_results:
405 logging.warning('Ignoring duplicate shard index %d', shard_index)
406 return
407 self._per_shard_results[shard_index] = result
408
409 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700410 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400411 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700412 result['outputs_ref']['isolatedserver'],
413 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400414 if storage:
415 # Output files are supposed to be small and they are not reused across
416 # tasks. So use MemoryCache for them instead of on-disk cache. Make
417 # files writable, so that calling script can delete them.
418 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700419 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400420 storage,
421 isolateserver.MemoryCache(file_mode_mask=0700),
422 os.path.join(self.task_output_dir, str(shard_index)),
423 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424
425 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700426 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427 with self._lock:
428 # Write an array of shard results with None for missing shards.
429 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430 'shards': [
431 self._per_shard_results.get(i) for i in xrange(self.shard_count)
432 ],
433 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700434 # Write summary.json to task_output_dir as well.
435 if self.task_output_dir:
436 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700437 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700438 summary,
439 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700440 if self._storage:
441 self._storage.close()
442 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700443 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444
445 def _get_storage(self, isolate_server, namespace):
446 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700447 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 with self._lock:
449 if not self._storage:
450 self._storage = isolateserver.get_storage(isolate_server, namespace)
451 else:
452 # Shards must all use exact same isolate server and namespace.
453 if self._storage.location != isolate_server:
454 logging.error(
455 'Task shards are using multiple isolate servers: %s and %s',
456 self._storage.location, isolate_server)
457 return None
458 if self._storage.namespace != namespace:
459 logging.error(
460 'Task shards are using multiple namespaces: %s and %s',
461 self._storage.namespace, namespace)
462 return None
463 return self._storage
464
465
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500466def now():
467 """Exists so it can be mocked easily."""
468 return time.time()
469
470
maruel77f720b2015-09-15 12:35:22 -0700471def parse_time(value):
472 """Converts serialized time from the API to datetime.datetime."""
473 # When microseconds are 0, the '.123456' suffix is elided. This means the
474 # serialized format is not consistent, which confuses the hell out of python.
475 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
476 try:
477 return datetime.datetime.strptime(value, fmt)
478 except ValueError:
479 pass
480 raise ValueError('Failed to parse %s' % value)
481
482
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700483def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400484 base_url, shard_index, task_id, timeout, should_stop, output_collector):
485 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700486
Vadim Shtayurab450c602014-05-12 19:23:25 -0700487 Returns:
488 <result dict> on success.
489 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700490 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000491 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700492 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
493 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494 started = now()
495 deadline = started + timeout if timeout else None
496 attempt = 0
497
498 while not should_stop.is_set():
499 attempt += 1
500
501 # Waiting for too long -> give up.
502 current_time = now()
503 if deadline and current_time >= deadline:
504 logging.error('retrieve_results(%s) timed out on attempt %d',
505 base_url, attempt)
506 return None
507
508 # Do not spin too fast. Spin faster at the beginning though.
509 # Start with 1 sec delay and for each 30 sec of waiting add another second
510 # of delay, until hitting 15 sec ceiling.
511 if attempt > 1:
512 max_delay = min(15, 1 + (current_time - started) / 30.0)
513 delay = min(max_delay, deadline - current_time) if deadline else max_delay
514 if delay > 0:
515 logging.debug('Waiting %.1f sec before retrying', delay)
516 should_stop.wait(delay)
517 if should_stop.is_set():
518 return None
519
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400520 # Disable internal retries in net.url_read_json, since we are doing retries
521 # ourselves.
522 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700523 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
524 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400525 result = net.url_read_json(result_url, retry_50x=False)
526 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400527 continue
maruel77f720b2015-09-15 12:35:22 -0700528
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400529 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700530 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700532 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700533 # Record the result, try to fetch attached output files (if any).
534 if output_collector:
535 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700536 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700537 if result.get('internal_failure'):
538 logging.error('Internal error!')
539 elif result['state'] == 'BOT_DIED':
540 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700541 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000542
543
maruel77f720b2015-09-15 12:35:22 -0700544def convert_to_old_format(result):
545 """Converts the task result data from Endpoints API format to old API format
546 for compatibility.
547
548 This goes into the file generated as --task-summary-json.
549 """
550 # Sets default.
551 result.setdefault('abandoned_ts', None)
552 result.setdefault('bot_id', None)
553 result.setdefault('bot_version', None)
554 result.setdefault('children_task_ids', [])
555 result.setdefault('completed_ts', None)
556 result.setdefault('cost_saved_usd', None)
557 result.setdefault('costs_usd', None)
558 result.setdefault('deduped_from', None)
559 result.setdefault('name', None)
560 result.setdefault('outputs_ref', None)
561 result.setdefault('properties_hash', None)
562 result.setdefault('server_versions', None)
563 result.setdefault('started_ts', None)
564 result.setdefault('tags', None)
565 result.setdefault('user', None)
566
567 # Convertion back to old API.
568 duration = result.pop('duration', None)
569 result['durations'] = [duration] if duration else []
570 exit_code = result.pop('exit_code', None)
571 result['exit_codes'] = [int(exit_code)] if exit_code else []
572 result['id'] = result.pop('task_id')
573 result['isolated_out'] = result.get('outputs_ref', None)
574 output = result.pop('output', None)
575 result['outputs'] = [output] if output else []
576 # properties_hash
577 # server_version
578 # Endpoints result 'state' as string. For compatibility with old code, convert
579 # to int.
580 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700581 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700582 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700583 if 'bot_dimensions' in result:
584 result['bot_dimensions'] = {
585 i['key']: i['value'] for i in result['bot_dimensions']
586 }
587 else:
588 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700589
590
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700591def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400592 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
593 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500594 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000595
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700596 Duplicate shards are ignored. Shards are yielded in order of completion.
597 Timed out shards are NOT yielded at all. Caller can compare number of yielded
598 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000599
600 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500601 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 +0000602 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500603
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700604 output_collector is an optional instance of TaskOutputCollector that will be
605 used to fetch files produced by a task from isolate server to the local disk.
606
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500607 Yields:
608 (index, result). In particular, 'result' is defined as the
609 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000610 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000611 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400612 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700613 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700614 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700615
maruel@chromium.org0437a732013-08-27 16:05:52 +0000616 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
617 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700618 # Adds a task to the thread pool to call 'retrieve_results' and return
619 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400620 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700621 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000622 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400623 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
624 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700625
626 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400627 for shard_index, task_id in enumerate(task_ids):
628 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700629
630 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400631 shards_remaining = range(len(task_ids))
632 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700633 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700635 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700636 shard_index, result = results_channel.pull(
637 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700638 except threading_utils.TaskChannel.Timeout:
639 if print_status_updates:
640 print(
641 'Waiting for results from the following shards: %s' %
642 ', '.join(map(str, shards_remaining)))
643 sys.stdout.flush()
644 continue
645 except Exception:
646 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700647
648 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500651 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000652 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654 # Yield back results to the caller.
655 assert shard_index in shards_remaining
656 shards_remaining.remove(shard_index)
657 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700658
maruel@chromium.org0437a732013-08-27 16:05:52 +0000659 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000661 should_stop.set()
662
663
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400664def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000665 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700666 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400667 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700668 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
669 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400670 else:
671 pending = 'N/A'
672
maruel77f720b2015-09-15 12:35:22 -0700673 if metadata.get('duration') is not None:
674 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400675 else:
676 duration = 'N/A'
677
maruel77f720b2015-09-15 12:35:22 -0700678 if metadata.get('exit_code') is not None:
679 # Integers are encoded as string to not loose precision.
680 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400681 else:
682 exit_code = 'N/A'
683
684 bot_id = metadata.get('bot_id') or 'N/A'
685
maruel77f720b2015-09-15 12:35:22 -0700686 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400687 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400688 tag_footer = (
689 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
690 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400691
692 tag_len = max(len(tag_header), len(tag_footer))
693 dash_pad = '+-%s-+\n' % ('-' * tag_len)
694 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
695 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
696
697 header = dash_pad + tag_header + dash_pad
698 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700699 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400700 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000701
702
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700703def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700704 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400705 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700706 """Retrieves results of a Swarming task.
707
708 Returns:
709 process exit code that should be returned to the user.
710 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700711 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700712 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700713
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700714 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700715 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400716 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700717 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400718 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400719 swarming, task_ids, timeout, None, print_status_updates,
720 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700721 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700722
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400723 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700724 shard_exit_code = metadata.get('exit_code')
725 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700726 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700727 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700728 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400729 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700730 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700731
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700732 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400733 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400734 if len(seen_shards) < len(task_ids):
735 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700736 else:
maruel77f720b2015-09-15 12:35:22 -0700737 print('%s: %s %s' % (
738 metadata.get('bot_id', 'N/A'),
739 metadata['task_id'],
740 shard_exit_code))
741 if metadata['output']:
742 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400743 if output:
744 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700745 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700746 summary = output_collector.finalize()
747 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700748 # TODO(maruel): Make this optional.
749 for i in summary['shards']:
750 if i:
751 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700752 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700753
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400754 if decorate and total_duration:
755 print('Total duration: %.1fs' % total_duration)
756
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400757 if len(seen_shards) != len(task_ids):
758 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700759 print >> sys.stderr, ('Results from some shards are missing: %s' %
760 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700761 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700762
maruela5490782015-09-30 10:56:59 -0700763 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000764
765
maruel77f720b2015-09-15 12:35:22 -0700766### API management.
767
768
769class APIError(Exception):
770 pass
771
772
773def endpoints_api_discovery_apis(host):
774 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
775 the APIs exposed by a host.
776
777 https://developers.google.com/discovery/v1/reference/apis/list
778 """
779 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
780 if data is None:
781 raise APIError('Failed to discover APIs on %s' % host)
782 out = {}
783 for api in data['items']:
784 if api['id'] == 'discovery:v1':
785 continue
786 # URL is of the following form:
787 # url = host + (
788 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
789 api_data = net.url_read_json(api['discoveryRestUrl'])
790 if api_data is None:
791 raise APIError('Failed to discover %s on %s' % (api['id'], host))
792 out[api['id']] = api_data
793 return out
794
795
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500796### Commands.
797
798
799def abort_task(_swarming, _manifest):
800 """Given a task manifest that was triggered, aborts its execution."""
801 # TODO(vadimsh): No supported by the server yet.
802
803
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400804def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400805 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500806 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500807 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500808 dest='dimensions', metavar='FOO bar',
809 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500810 parser.add_option_group(parser.filter_group)
811
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400812
Vadim Shtayurab450c602014-05-12 19:23:25 -0700813def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400814 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700815 parser.sharding_group.add_option(
816 '--shards', type='int', default=1,
817 help='Number of shards to trigger and collect.')
818 parser.add_option_group(parser.sharding_group)
819
820
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400821def add_trigger_options(parser):
822 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500823 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400824 add_filter_options(parser)
825
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400826 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500827 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500828 '-s', '--isolated',
829 help='Hash of the .isolated to grab from the isolate server')
830 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500831 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700832 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500833 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500834 '--priority', type='int', default=100,
835 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500836 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500837 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400838 help='Display name of the task. Defaults to '
839 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
840 'isolated file is provided, if a hash is provided, it defaults to '
841 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400842 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400843 '--tags', action='append', default=[],
844 help='Tags to assign to the task.')
845 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500846 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400847 help='User associated with the task. Defaults to authenticated user on '
848 'the server.')
849 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400850 '--idempotent', action='store_true', default=False,
851 help='When set, the server will actively try to find a previous task '
852 'with the same parameter and return this result instead if possible')
853 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400854 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400855 help='Seconds to allow the task to be pending for a bot to run before '
856 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400857 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400858 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400859 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400860 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400861 '--hard-timeout', type='int', default=60*60,
862 help='Seconds to allow the task to complete.')
863 parser.task_group.add_option(
864 '--io-timeout', type='int', default=20*60,
865 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500866 parser.task_group.add_option(
867 '--raw-cmd', action='store_true', default=False,
868 help='When set, the command after -- is used as-is without run_isolated. '
869 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500870 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000871
872
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500873def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500874 """Processes trigger options and uploads files to isolate server if necessary.
875 """
876 options.dimensions = dict(options.dimensions)
877 options.env = dict(options.env)
878
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500879 if not options.dimensions:
880 parser.error('Please at least specify one --dimension')
881 if options.raw_cmd:
882 if not args:
883 parser.error(
884 'Arguments with --raw-cmd should be passed after -- as command '
885 'delimiter.')
886 if options.isolate_server:
887 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
888
889 command = args
890 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500891 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500892 options.user,
893 '_'.join(
894 '%s=%s' % (k, v)
895 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700896 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500897 else:
898 isolateserver.process_isolate_server_options(parser, options, False)
899 try:
maruel77f720b2015-09-15 12:35:22 -0700900 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500901 except ValueError as e:
902 parser.error(str(e))
903
maruel77f720b2015-09-15 12:35:22 -0700904 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
905 # actual command to run.
906 properties = TaskProperties(
907 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500908 dimensions=options.dimensions,
909 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700910 execution_timeout_secs=options.hard_timeout,
911 extra_args=command if inputs_ref else None,
912 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500913 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700914 inputs_ref=inputs_ref,
915 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700916 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
917 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700918 return NewTaskRequest(
919 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500920 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700921 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500922 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700923 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500924 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700925 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000926
927
928def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500929 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000930 '-t', '--timeout',
931 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400932 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000933 help='Timeout to wait for result, set to 0 for no timeout; default: '
934 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500935 parser.group_logging.add_option(
936 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700937 parser.group_logging.add_option(
938 '--print-status-updates', action='store_true',
939 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400940 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700941 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700942 '--task-summary-json',
943 metavar='FILE',
944 help='Dump a summary of task results to this file as json. It contains '
945 'only shards statuses as know to server directly. Any output files '
946 'emitted by the task can be collected by using --task-output-dir')
947 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700948 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700949 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700950 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700951 'directory contains per-shard directory with output files produced '
952 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700953 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000954
955
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400956@subcommand.usage('bots...')
957def CMDbot_delete(parser, args):
958 """Forcibly deletes bots from the Swarming server."""
959 parser.add_option(
960 '-f', '--force', action='store_true',
961 help='Do not prompt for confirmation')
962 options, args = parser.parse_args(args)
963 if not args:
964 parser.error('Please specific bots to delete')
965
966 bots = sorted(args)
967 if not options.force:
968 print('Delete the following bots?')
969 for bot in bots:
970 print(' %s' % bot)
971 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
972 print('Goodbye.')
973 return 1
974
975 result = 0
976 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -0700977 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
978 if net.url_read_json(url, data={}, method='POST') is None:
979 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400980 result = 1
981 return result
982
983
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400984def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400985 """Returns information about the bots connected to the Swarming server."""
986 add_filter_options(parser)
987 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400988 '--dead-only', action='store_true',
989 help='Only print dead bots, useful to reap them and reimage broken bots')
990 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400991 '-k', '--keep-dead', action='store_true',
992 help='Do not filter out dead bots')
993 parser.filter_group.add_option(
994 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400995 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400996 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400997
998 if options.keep_dead and options.dead_only:
999 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001000
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001001 bots = []
1002 cursor = None
1003 limit = 250
1004 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001005 base_url = (
1006 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001007 while True:
1008 url = base_url
1009 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001010 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001011 data = net.url_read_json(url)
1012 if data is None:
1013 print >> sys.stderr, 'Failed to access %s' % options.swarming
1014 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001015 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001016 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001017 if not cursor:
1018 break
1019
maruel77f720b2015-09-15 12:35:22 -07001020 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001021 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001022 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001023 continue
maruel77f720b2015-09-15 12:35:22 -07001024 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001025 continue
1026
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001027 # If the user requested to filter on dimensions, ensure the bot has all the
1028 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001029 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001030 for key, value in options.dimensions:
1031 if key not in dimensions:
1032 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001033 # A bot can have multiple value for a key, for example,
1034 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1035 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001036 if isinstance(dimensions[key], list):
1037 if value not in dimensions[key]:
1038 break
1039 else:
1040 if value != dimensions[key]:
1041 break
1042 else:
maruel77f720b2015-09-15 12:35:22 -07001043 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001044 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001045 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001046 if bot.get('task_id'):
1047 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001048 return 0
1049
1050
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001051@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001052def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001053 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001054
1055 The result can be in multiple part if the execution was sharded. It can
1056 potentially have retries.
1057 """
1058 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001059 parser.add_option(
1060 '-j', '--json',
1061 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001062 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001063 if not args and not options.json:
1064 parser.error('Must specify at least one task id or --json.')
1065 if args and options.json:
1066 parser.error('Only use one of task id or --json.')
1067
1068 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001069 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001070 try:
maruel1ceb3872015-10-14 06:10:44 -07001071 with fs.open(options.json, 'rb') as f:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001072 tasks = sorted(
1073 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1074 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001075 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001076 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001077 else:
1078 valid = frozenset('0123456789abcdef')
1079 if any(not valid.issuperset(task_id) for task_id in args):
1080 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001081
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001082 try:
1083 return collect(
1084 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001085 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001086 options.timeout,
1087 options.decorate,
1088 options.print_status_updates,
1089 options.task_summary_json,
1090 options.task_output_dir)
1091 except Failure:
1092 on_error.report(None)
1093 return 1
1094
1095
maruelbea00862015-09-18 09:55:36 -07001096@subcommand.usage('[filename]')
1097def CMDput_bootstrap(parser, args):
1098 """Uploads a new version of bootstrap.py."""
1099 options, args = parser.parse_args(args)
1100 if len(args) != 1:
1101 parser.error('Must specify file to upload')
1102 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001103 path = unicode(os.path.abspath(args[0]))
1104 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001105 content = f.read().decode('utf-8')
1106 data = net.url_read_json(url, data={'content': content})
1107 print data
1108 return 0
1109
1110
1111@subcommand.usage('[filename]')
1112def CMDput_bot_config(parser, args):
1113 """Uploads a new version of bot_config.py."""
1114 options, args = parser.parse_args(args)
1115 if len(args) != 1:
1116 parser.error('Must specify file to upload')
1117 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001118 path = unicode(os.path.abspath(args[0]))
1119 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001120 content = f.read().decode('utf-8')
1121 data = net.url_read_json(url, data={'content': content})
1122 print data
1123 return 0
1124
1125
maruel77f720b2015-09-15 12:35:22 -07001126@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001127def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001128 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1129 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001130
1131 Examples:
maruel77f720b2015-09-15 12:35:22 -07001132 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001133 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001134
maruel77f720b2015-09-15 12:35:22 -07001135 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001136 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1137
1138 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1139 quoting is important!:
1140 swarming.py query -S server-url.com --limit 10 \\
1141 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001142 """
1143 CHUNK_SIZE = 250
1144
1145 parser.add_option(
1146 '-L', '--limit', type='int', default=200,
1147 help='Limit to enforce on limitless items (like number of tasks); '
1148 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001149 parser.add_option(
1150 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001151 parser.add_option(
1152 '--progress', action='store_true',
1153 help='Prints a dot at each request to show progress')
1154 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001155 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001156 parser.error(
1157 'Must specify only method name and optionally query args properly '
1158 'escaped.')
1159 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001160 url = base_url
1161 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001162 # Check check, change if not working out.
1163 merge_char = '&' if '?' in url else '?'
1164 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001165 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001166 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001167 # TODO(maruel): Do basic diagnostic.
1168 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001169 return 1
1170
1171 # Some items support cursors. Try to get automatically if cursors are needed
1172 # by looking at the 'cursor' items.
1173 while (
1174 data.get('cursor') and
1175 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001176 merge_char = '&' if '?' in base_url else '?'
1177 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001178 if options.limit:
1179 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001180 if options.progress:
1181 sys.stdout.write('.')
1182 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001183 new = net.url_read_json(url)
1184 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001185 if options.progress:
1186 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001187 print >> sys.stderr, 'Failed to access %s' % options.swarming
1188 return 1
maruel81b37132015-10-21 06:42:13 -07001189 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001190 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001191
maruel77f720b2015-09-15 12:35:22 -07001192 if options.progress:
1193 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001194 if options.limit and len(data.get('items', [])) > options.limit:
1195 data['items'] = data['items'][:options.limit]
1196 data.pop('cursor', None)
1197
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001198 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001199 options.json = unicode(os.path.abspath(options.json))
1200 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001201 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001202 try:
maruel77f720b2015-09-15 12:35:22 -07001203 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001204 sys.stdout.write('\n')
1205 except IOError:
1206 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001207 return 0
1208
1209
maruel77f720b2015-09-15 12:35:22 -07001210def CMDquery_list(parser, args):
1211 """Returns list of all the Swarming APIs that can be used with command
1212 'query'.
1213 """
1214 parser.add_option(
1215 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1216 options, args = parser.parse_args(args)
1217 if args:
1218 parser.error('No argument allowed.')
1219
1220 try:
1221 apis = endpoints_api_discovery_apis(options.swarming)
1222 except APIError as e:
1223 parser.error(str(e))
1224 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001225 options.json = unicode(os.path.abspath(options.json))
1226 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001227 json.dump(apis, f)
1228 else:
1229 help_url = (
1230 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1231 options.swarming)
1232 for api_id, api in sorted(apis.iteritems()):
1233 print api_id
1234 print ' ' + api['description']
1235 for resource_name, resource in sorted(api['resources'].iteritems()):
1236 print ''
1237 for method_name, method in sorted(resource['methods'].iteritems()):
1238 # Only list the GET ones.
1239 if method['httpMethod'] != 'GET':
1240 continue
1241 print '- %s.%s: %s' % (
1242 resource_name, method_name, method['path'])
1243 print ' ' + method['description']
1244 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1245 return 0
1246
1247
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001248@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001249def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001250 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001251
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001252 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001253 """
1254 add_trigger_options(parser)
1255 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001256 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001257 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001258 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001259 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001260 tasks = trigger_task_shards(
1261 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001262 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001263 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001264 'Failed to trigger %s(%s): %s' %
1265 (options.task_name, args[0], e.args[0]))
1266 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001267 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001268 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001269 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001270 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001271 task_ids = [
1272 t['task_id']
1273 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1274 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001275 try:
1276 return collect(
1277 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001278 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001279 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001280 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001281 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001282 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001283 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001284 except Failure:
1285 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001286 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001287
1288
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001289@subcommand.usage('task_id')
1290def CMDreproduce(parser, args):
1291 """Runs a task locally that was triggered on the server.
1292
1293 This running locally the same commands that have been run on the bot. The data
1294 downloaded will be in a subdirectory named 'work' of the current working
1295 directory.
1296 """
1297 options, args = parser.parse_args(args)
1298 if len(args) != 1:
1299 parser.error('Must specify exactly one task id.')
1300
maruel77f720b2015-09-15 12:35:22 -07001301 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001302 request = net.url_read_json(url)
1303 if not request:
1304 print >> sys.stderr, 'Failed to retrieve request data for the task'
1305 return 1
1306
maruel12e30012015-10-09 11:55:35 -07001307 workdir = unicode(os.path.abspath('work'))
1308 if not fs.isdir(workdir):
1309 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001310
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001311 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001312 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001313 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001314 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001315 logging.info('env: %r', properties['env'])
1316 env.update(
maruel77f720b2015-09-15 12:35:22 -07001317 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1318 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001319
maruel29ab2fd2015-10-16 11:44:01 -07001320 if properties.get('inputs_ref'):
1321 # Create the tree.
1322 with isolateserver.get_storage(
1323 properties['inputs_ref']['isolatedserver'],
1324 properties['inputs_ref']['namespace']) as storage:
1325 bundle = isolateserver.fetch_isolated(
1326 properties['inputs_ref']['isolated'],
1327 storage,
1328 isolateserver.MemoryCache(file_mode_mask=0700),
1329 workdir,
1330 False)
1331 command = bundle.command
1332 if bundle.relative_cwd:
1333 workdir = os.path.join(workdir, bundle.relative_cwd)
1334 else:
1335 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001336 try:
maruel29ab2fd2015-10-16 11:44:01 -07001337 return subprocess.call(command, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001338 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001339 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001340 print >> sys.stderr, str(e)
1341 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001342
1343
maruel0eb1d1b2015-10-02 14:48:21 -07001344@subcommand.usage('bot_id')
1345def CMDterminate(parser, args):
1346 """Tells a bot to gracefully shut itself down as soon as it can.
1347
1348 This is done by completing whatever current task there is then exiting the bot
1349 process.
1350 """
1351 parser.add_option(
1352 '--wait', action='store_true', help='Wait for the bot to terminate')
1353 options, args = parser.parse_args(args)
1354 if len(args) != 1:
1355 parser.error('Please provide the bot id')
1356 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1357 request = net.url_read_json(url, data={})
1358 if not request:
1359 print >> sys.stderr, 'Failed to ask for termination'
1360 return 1
1361 if options.wait:
1362 return collect(
1363 options.swarming, [request['task_id']], 0., False, False, None, None)
1364 return 0
1365
1366
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001367@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001368def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001369 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001370
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001371 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001372 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001373
1374 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001375
1376 Passes all extra arguments provided after '--' as additional command line
1377 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001378 """
1379 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001380 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001381 parser.add_option(
1382 '--dump-json',
1383 metavar='FILE',
1384 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001385 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001386 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001387 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001388 tasks = trigger_task_shards(
1389 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001390 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001391 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001392 tasks_sorted = sorted(
1393 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001394 if options.dump_json:
1395 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001396 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001397 'tasks': tasks,
1398 }
maruel46b015f2015-10-13 18:40:35 -07001399 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001400 print('To collect results, use:')
1401 print(' swarming.py collect -S %s --json %s' %
1402 (options.swarming, options.dump_json))
1403 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001404 print('To collect results, use:')
1405 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001406 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1407 print('Or visit:')
1408 for t in tasks_sorted:
1409 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001410 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001411 except Failure:
1412 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001413 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001414
1415
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001416class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001417 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001418 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001419 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001420 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001421 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001422 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001423 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001424 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001425 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001426 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001427
1428 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001429 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001430 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001431 auth.process_auth_options(self, options)
1432 user = self._process_swarming(options)
1433 if hasattr(options, 'user') and not options.user:
1434 options.user = user
1435 return options, args
1436
1437 def _process_swarming(self, options):
1438 """Processes the --swarming option and aborts if not specified.
1439
1440 Returns the identity as determined by the server.
1441 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001442 if not options.swarming:
1443 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001444 try:
1445 options.swarming = net.fix_url(options.swarming)
1446 except ValueError as e:
1447 self.error('--swarming %s' % e)
1448 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001449 try:
1450 user = auth.ensure_logged_in(options.swarming)
1451 except ValueError as e:
1452 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001453 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001454
1455
1456def main(args):
1457 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001458 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001459
1460
1461if __name__ == '__main__':
1462 fix_encoding.fix_encoding()
1463 tools.disable_buffering()
1464 colorama.init()
1465 sys.exit(main(sys.argv[1:]))