blob: d6eccbd8b876b78f5ebc3d3b95693a606d90d73e [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
maruelb76604c2015-11-11 11:53:44 -08008__version__ = '0.8.4'
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
marueld4d15312015-11-16 17:22:59 -0800222 if result.get('errors'):
223 # The reply is an error.
224 on_error.report(
225 'Failed to trigger task %s\n%s' %
226 (raw_request['name'], result['errors']))
227 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500228 return result
229
230
231def setup_googletest(env, shards, index):
232 """Sets googletest specific environment variables."""
233 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700234 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
235 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
236 env = env[:]
237 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
238 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500239 return env
240
241
242def trigger_task_shards(swarming, task_request, shards):
243 """Triggers one or many subtasks of a sharded task.
244
245 Returns:
246 Dict with task details, returned to caller as part of --dump-json output.
247 None in case of failure.
248 """
249 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700250 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700252 req['properties']['env'] = setup_googletest(
253 req['properties']['env'], shards, index)
254 req['name'] += ':%s:%s' % (index, shards)
255 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500256
257 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500258 tasks = {}
259 priority_warning = False
260 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700261 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500262 if not task:
263 break
264 logging.info('Request result: %s', task)
265 if (not priority_warning and
266 task['request']['priority'] != task_request.priority):
267 priority_warning = True
268 print >> sys.stderr, (
269 'Priority was reset to %s' % task['request']['priority'])
270 tasks[request['name']] = {
271 'shard_index': index,
272 'task_id': task['task_id'],
273 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
274 }
275
276 # Some shards weren't triggered. Abort everything.
277 if len(tasks) != len(requests):
278 if tasks:
279 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
280 len(tasks), len(requests))
281 for task_dict in tasks.itervalues():
282 abort_task(swarming, task_dict['task_id'])
283 return None
284
285 return tasks
286
287
288### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000289
290
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700291# How often to print status updates to stdout in 'collect'.
292STATUS_UPDATE_INTERVAL = 15 * 60.
293
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400294
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400295class State(object):
296 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000297
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400298 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
299 values are part of the API so if they change, the API changed.
300
301 It's in fact an enum. Values should be in decreasing order of importance.
302 """
303 RUNNING = 0x10
304 PENDING = 0x20
305 EXPIRED = 0x30
306 TIMED_OUT = 0x40
307 BOT_DIED = 0x50
308 CANCELED = 0x60
309 COMPLETED = 0x70
310
maruel77f720b2015-09-15 12:35:22 -0700311 STATES = (
312 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
313 'COMPLETED')
314 STATES_RUNNING = ('RUNNING', 'PENDING')
315 STATES_NOT_RUNNING = (
316 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
317 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
318 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400319
320 _NAMES = {
321 RUNNING: 'Running',
322 PENDING: 'Pending',
323 EXPIRED: 'Expired',
324 TIMED_OUT: 'Execution timed out',
325 BOT_DIED: 'Bot died',
326 CANCELED: 'User canceled',
327 COMPLETED: 'Completed',
328 }
329
maruel77f720b2015-09-15 12:35:22 -0700330 _ENUMS = {
331 'RUNNING': RUNNING,
332 'PENDING': PENDING,
333 'EXPIRED': EXPIRED,
334 'TIMED_OUT': TIMED_OUT,
335 'BOT_DIED': BOT_DIED,
336 'CANCELED': CANCELED,
337 'COMPLETED': COMPLETED,
338 }
339
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400340 @classmethod
341 def to_string(cls, state):
342 """Returns a user-readable string representing a State."""
343 if state not in cls._NAMES:
344 raise ValueError('Invalid state %s' % state)
345 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000346
maruel77f720b2015-09-15 12:35:22 -0700347 @classmethod
348 def from_enum(cls, state):
349 """Returns int value based on the string."""
350 if state not in cls._ENUMS:
351 raise ValueError('Invalid state %s' % state)
352 return cls._ENUMS[state]
353
maruel@chromium.org0437a732013-08-27 16:05:52 +0000354
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700355class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700356 """Assembles task execution summary (for --task-summary-json output).
357
358 Optionally fetches task outputs from isolate server to local disk (used when
359 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360
361 This object is shared among multiple threads running 'retrieve_results'
362 function, in particular they call 'process_shard_result' method in parallel.
363 """
364
maruel0eb1d1b2015-10-02 14:48:21 -0700365 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700366 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
367
368 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700369 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370 shard_count: expected number of task shards.
371 """
maruel12e30012015-10-09 11:55:35 -0700372 self.task_output_dir = (
373 unicode(os.path.abspath(task_output_dir))
374 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700375 self.shard_count = shard_count
376
377 self._lock = threading.Lock()
378 self._per_shard_results = {}
379 self._storage = None
380
maruel12e30012015-10-09 11:55:35 -0700381 if self.task_output_dir and not fs.isdir(self.task_output_dir):
382 fs.makedirs(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700383
Vadim Shtayurab450c602014-05-12 19:23:25 -0700384 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 """Stores results of a single task shard, fetches output files if necessary.
386
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400387 Modifies |result| in place.
388
maruel77f720b2015-09-15 12:35:22 -0700389 shard_index is 0-based.
390
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700391 Called concurrently from multiple threads.
392 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700393 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700394 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700395 if shard_index < 0 or shard_index >= self.shard_count:
396 logging.warning(
397 'Shard index %d is outside of expected range: [0; %d]',
398 shard_index, self.shard_count - 1)
399 return
400
maruel77f720b2015-09-15 12:35:22 -0700401 if result.get('outputs_ref'):
402 ref = result['outputs_ref']
403 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
404 ref['isolatedserver'],
405 urllib.urlencode(
406 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400407
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700408 # Store result dict of that shard, ignore results we've already seen.
409 with self._lock:
410 if shard_index in self._per_shard_results:
411 logging.warning('Ignoring duplicate shard index %d', shard_index)
412 return
413 self._per_shard_results[shard_index] = result
414
415 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700416 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400417 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700418 result['outputs_ref']['isolatedserver'],
419 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400420 if storage:
421 # Output files are supposed to be small and they are not reused across
422 # tasks. So use MemoryCache for them instead of on-disk cache. Make
423 # files writable, so that calling script can delete them.
424 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700425 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400426 storage,
427 isolateserver.MemoryCache(file_mode_mask=0700),
428 os.path.join(self.task_output_dir, str(shard_index)),
429 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430
431 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700432 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 with self._lock:
434 # Write an array of shard results with None for missing shards.
435 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700436 'shards': [
437 self._per_shard_results.get(i) for i in xrange(self.shard_count)
438 ],
439 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 # Write summary.json to task_output_dir as well.
441 if self.task_output_dir:
442 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700443 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700444 summary,
445 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700446 if self._storage:
447 self._storage.close()
448 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700449 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700450
451 def _get_storage(self, isolate_server, namespace):
452 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700453 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 with self._lock:
455 if not self._storage:
456 self._storage = isolateserver.get_storage(isolate_server, namespace)
457 else:
458 # Shards must all use exact same isolate server and namespace.
459 if self._storage.location != isolate_server:
460 logging.error(
461 'Task shards are using multiple isolate servers: %s and %s',
462 self._storage.location, isolate_server)
463 return None
464 if self._storage.namespace != namespace:
465 logging.error(
466 'Task shards are using multiple namespaces: %s and %s',
467 self._storage.namespace, namespace)
468 return None
469 return self._storage
470
471
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500472def now():
473 """Exists so it can be mocked easily."""
474 return time.time()
475
476
maruel77f720b2015-09-15 12:35:22 -0700477def parse_time(value):
478 """Converts serialized time from the API to datetime.datetime."""
479 # When microseconds are 0, the '.123456' suffix is elided. This means the
480 # serialized format is not consistent, which confuses the hell out of python.
481 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
482 try:
483 return datetime.datetime.strptime(value, fmt)
484 except ValueError:
485 pass
486 raise ValueError('Failed to parse %s' % value)
487
488
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700489def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400490 base_url, shard_index, task_id, timeout, should_stop, output_collector):
491 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700492
Vadim Shtayurab450c602014-05-12 19:23:25 -0700493 Returns:
494 <result dict> on success.
495 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700496 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000497 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700498 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
499 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700500 started = now()
501 deadline = started + timeout if timeout else None
502 attempt = 0
503
504 while not should_stop.is_set():
505 attempt += 1
506
507 # Waiting for too long -> give up.
508 current_time = now()
509 if deadline and current_time >= deadline:
510 logging.error('retrieve_results(%s) timed out on attempt %d',
511 base_url, attempt)
512 return None
513
514 # Do not spin too fast. Spin faster at the beginning though.
515 # Start with 1 sec delay and for each 30 sec of waiting add another second
516 # of delay, until hitting 15 sec ceiling.
517 if attempt > 1:
518 max_delay = min(15, 1 + (current_time - started) / 30.0)
519 delay = min(max_delay, deadline - current_time) if deadline else max_delay
520 if delay > 0:
521 logging.debug('Waiting %.1f sec before retrying', delay)
522 should_stop.wait(delay)
523 if should_stop.is_set():
524 return None
525
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400526 # Disable internal retries in net.url_read_json, since we are doing retries
527 # ourselves.
528 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700529 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
530 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 result = net.url_read_json(result_url, retry_50x=False)
532 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400533 continue
maruel77f720b2015-09-15 12:35:22 -0700534
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400535 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700536 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400537 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700538 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700539 # Record the result, try to fetch attached output files (if any).
540 if output_collector:
541 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700542 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700543 if result.get('internal_failure'):
544 logging.error('Internal error!')
545 elif result['state'] == 'BOT_DIED':
546 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700547 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000548
549
maruel77f720b2015-09-15 12:35:22 -0700550def convert_to_old_format(result):
551 """Converts the task result data from Endpoints API format to old API format
552 for compatibility.
553
554 This goes into the file generated as --task-summary-json.
555 """
556 # Sets default.
557 result.setdefault('abandoned_ts', None)
558 result.setdefault('bot_id', None)
559 result.setdefault('bot_version', None)
560 result.setdefault('children_task_ids', [])
561 result.setdefault('completed_ts', None)
562 result.setdefault('cost_saved_usd', None)
563 result.setdefault('costs_usd', None)
564 result.setdefault('deduped_from', None)
565 result.setdefault('name', None)
566 result.setdefault('outputs_ref', None)
567 result.setdefault('properties_hash', None)
568 result.setdefault('server_versions', None)
569 result.setdefault('started_ts', None)
570 result.setdefault('tags', None)
571 result.setdefault('user', None)
572
573 # Convertion back to old API.
574 duration = result.pop('duration', None)
575 result['durations'] = [duration] if duration else []
576 exit_code = result.pop('exit_code', None)
577 result['exit_codes'] = [int(exit_code)] if exit_code else []
578 result['id'] = result.pop('task_id')
579 result['isolated_out'] = result.get('outputs_ref', None)
580 output = result.pop('output', None)
581 result['outputs'] = [output] if output else []
582 # properties_hash
583 # server_version
584 # Endpoints result 'state' as string. For compatibility with old code, convert
585 # to int.
586 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700587 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700588 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700589 if 'bot_dimensions' in result:
590 result['bot_dimensions'] = {
591 i['key']: i['value'] for i in result['bot_dimensions']
592 }
593 else:
594 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700595
596
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700597def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400598 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
599 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500600 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000601
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700602 Duplicate shards are ignored. Shards are yielded in order of completion.
603 Timed out shards are NOT yielded at all. Caller can compare number of yielded
604 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000605
606 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500607 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 +0000608 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500609
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700610 output_collector is an optional instance of TaskOutputCollector that will be
611 used to fetch files produced by a task from isolate server to the local disk.
612
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500613 Yields:
614 (index, result). In particular, 'result' is defined as the
615 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000616 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400618 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700619 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700620 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700621
maruel@chromium.org0437a732013-08-27 16:05:52 +0000622 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
623 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700624 # Adds a task to the thread pool to call 'retrieve_results' and return
625 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400626 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700627 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400629 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
630 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700631
632 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400633 for shard_index, task_id in enumerate(task_ids):
634 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700635
636 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400637 shards_remaining = range(len(task_ids))
638 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700639 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700640 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700641 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700642 shard_index, result = results_channel.pull(
643 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700644 except threading_utils.TaskChannel.Timeout:
645 if print_status_updates:
646 print(
647 'Waiting for results from the following shards: %s' %
648 ', '.join(map(str, shards_remaining)))
649 sys.stdout.flush()
650 continue
651 except Exception:
652 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653
654 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700655 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500657 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000658 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700659
Vadim Shtayurab450c602014-05-12 19:23:25 -0700660 # Yield back results to the caller.
661 assert shard_index in shards_remaining
662 shards_remaining.remove(shard_index)
663 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700664
maruel@chromium.org0437a732013-08-27 16:05:52 +0000665 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700666 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000667 should_stop.set()
668
669
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400670def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000671 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700672 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400673 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700674 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
675 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400676 else:
677 pending = 'N/A'
678
maruel77f720b2015-09-15 12:35:22 -0700679 if metadata.get('duration') is not None:
680 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400681 else:
682 duration = 'N/A'
683
maruel77f720b2015-09-15 12:35:22 -0700684 if metadata.get('exit_code') is not None:
685 # Integers are encoded as string to not loose precision.
686 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400687 else:
688 exit_code = 'N/A'
689
690 bot_id = metadata.get('bot_id') or 'N/A'
691
maruel77f720b2015-09-15 12:35:22 -0700692 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400693 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400694 tag_footer = (
695 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
696 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400697
698 tag_len = max(len(tag_header), len(tag_footer))
699 dash_pad = '+-%s-+\n' % ('-' * tag_len)
700 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
701 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
702
703 header = dash_pad + tag_header + dash_pad
704 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700705 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400706 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000707
708
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700709def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700710 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400711 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700712 """Retrieves results of a Swarming task.
713
714 Returns:
715 process exit code that should be returned to the user.
716 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700717 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700718 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700719
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700720 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700721 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400722 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700723 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400724 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400725 swarming, task_ids, timeout, None, print_status_updates,
726 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700727 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700728
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400729 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700730 shard_exit_code = metadata.get('exit_code')
731 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700732 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700733 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700734 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400735 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700736 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700737
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700738 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400739 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400740 if len(seen_shards) < len(task_ids):
741 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700742 else:
maruel77f720b2015-09-15 12:35:22 -0700743 print('%s: %s %s' % (
744 metadata.get('bot_id', 'N/A'),
745 metadata['task_id'],
746 shard_exit_code))
747 if metadata['output']:
748 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400749 if output:
750 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700751 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700752 summary = output_collector.finalize()
753 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700754 # TODO(maruel): Make this optional.
755 for i in summary['shards']:
756 if i:
757 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700758 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700759
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400760 if decorate and total_duration:
761 print('Total duration: %.1fs' % total_duration)
762
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400763 if len(seen_shards) != len(task_ids):
764 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700765 print >> sys.stderr, ('Results from some shards are missing: %s' %
766 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700767 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700768
maruela5490782015-09-30 10:56:59 -0700769 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000770
771
maruel77f720b2015-09-15 12:35:22 -0700772### API management.
773
774
775class APIError(Exception):
776 pass
777
778
779def endpoints_api_discovery_apis(host):
780 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
781 the APIs exposed by a host.
782
783 https://developers.google.com/discovery/v1/reference/apis/list
784 """
785 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
786 if data is None:
787 raise APIError('Failed to discover APIs on %s' % host)
788 out = {}
789 for api in data['items']:
790 if api['id'] == 'discovery:v1':
791 continue
792 # URL is of the following form:
793 # url = host + (
794 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
795 api_data = net.url_read_json(api['discoveryRestUrl'])
796 if api_data is None:
797 raise APIError('Failed to discover %s on %s' % (api['id'], host))
798 out[api['id']] = api_data
799 return out
800
801
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500802### Commands.
803
804
805def abort_task(_swarming, _manifest):
806 """Given a task manifest that was triggered, aborts its execution."""
807 # TODO(vadimsh): No supported by the server yet.
808
809
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400810def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400811 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500812 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500813 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500814 dest='dimensions', metavar='FOO bar',
815 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500816 parser.add_option_group(parser.filter_group)
817
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400818
Vadim Shtayurab450c602014-05-12 19:23:25 -0700819def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400820 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700821 parser.sharding_group.add_option(
822 '--shards', type='int', default=1,
823 help='Number of shards to trigger and collect.')
824 parser.add_option_group(parser.sharding_group)
825
826
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400827def add_trigger_options(parser):
828 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500829 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400830 add_filter_options(parser)
831
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400832 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500833 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500834 '-s', '--isolated',
835 help='Hash of the .isolated to grab from the isolate server')
836 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500837 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700838 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500839 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500840 '--priority', type='int', default=100,
841 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500842 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500843 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400844 help='Display name of the task. Defaults to '
845 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
846 'isolated file is provided, if a hash is provided, it defaults to '
847 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400848 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400849 '--tags', action='append', default=[],
850 help='Tags to assign to the task.')
851 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500852 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400853 help='User associated with the task. Defaults to authenticated user on '
854 'the server.')
855 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400856 '--idempotent', action='store_true', default=False,
857 help='When set, the server will actively try to find a previous task '
858 'with the same parameter and return this result instead if possible')
859 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400860 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400861 help='Seconds to allow the task to be pending for a bot to run before '
862 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400863 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400864 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400865 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400866 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400867 '--hard-timeout', type='int', default=60*60,
868 help='Seconds to allow the task to complete.')
869 parser.task_group.add_option(
870 '--io-timeout', type='int', default=20*60,
871 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500872 parser.task_group.add_option(
873 '--raw-cmd', action='store_true', default=False,
874 help='When set, the command after -- is used as-is without run_isolated. '
875 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500876 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000877
878
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500879def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500880 """Processes trigger options and uploads files to isolate server if necessary.
881 """
882 options.dimensions = dict(options.dimensions)
883 options.env = dict(options.env)
884
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500885 if not options.dimensions:
886 parser.error('Please at least specify one --dimension')
887 if options.raw_cmd:
888 if not args:
889 parser.error(
890 'Arguments with --raw-cmd should be passed after -- as command '
891 'delimiter.')
892 if options.isolate_server:
893 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
894
895 command = args
896 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500897 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500898 options.user,
899 '_'.join(
900 '%s=%s' % (k, v)
901 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700902 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500903 else:
904 isolateserver.process_isolate_server_options(parser, options, False)
905 try:
maruel77f720b2015-09-15 12:35:22 -0700906 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500907 except ValueError as e:
908 parser.error(str(e))
909
maruel77f720b2015-09-15 12:35:22 -0700910 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
911 # actual command to run.
912 properties = TaskProperties(
913 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500914 dimensions=options.dimensions,
915 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700916 execution_timeout_secs=options.hard_timeout,
917 extra_args=command if inputs_ref else None,
918 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500919 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700920 inputs_ref=inputs_ref,
921 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700922 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
923 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700924 return NewTaskRequest(
925 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500926 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700927 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500928 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700929 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500930 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700931 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000932
933
934def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500935 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000936 '-t', '--timeout',
937 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400938 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000939 help='Timeout to wait for result, set to 0 for no timeout; default: '
940 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500941 parser.group_logging.add_option(
942 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700943 parser.group_logging.add_option(
944 '--print-status-updates', action='store_true',
945 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400946 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700947 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700948 '--task-summary-json',
949 metavar='FILE',
950 help='Dump a summary of task results to this file as json. It contains '
951 'only shards statuses as know to server directly. Any output files '
952 'emitted by the task can be collected by using --task-output-dir')
953 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700954 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700955 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700956 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700957 'directory contains per-shard directory with output files produced '
958 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700959 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000960
961
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400962@subcommand.usage('bots...')
963def CMDbot_delete(parser, args):
964 """Forcibly deletes bots from the Swarming server."""
965 parser.add_option(
966 '-f', '--force', action='store_true',
967 help='Do not prompt for confirmation')
968 options, args = parser.parse_args(args)
969 if not args:
970 parser.error('Please specific bots to delete')
971
972 bots = sorted(args)
973 if not options.force:
974 print('Delete the following bots?')
975 for bot in bots:
976 print(' %s' % bot)
977 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
978 print('Goodbye.')
979 return 1
980
981 result = 0
982 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -0700983 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
984 if net.url_read_json(url, data={}, method='POST') is None:
985 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400986 result = 1
987 return result
988
989
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400990def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400991 """Returns information about the bots connected to the Swarming server."""
992 add_filter_options(parser)
993 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400994 '--dead-only', action='store_true',
995 help='Only print dead bots, useful to reap them and reimage broken bots')
996 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400997 '-k', '--keep-dead', action='store_true',
998 help='Do not filter out dead bots')
999 parser.filter_group.add_option(
1000 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001001 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001002 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001003
1004 if options.keep_dead and options.dead_only:
1005 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001006
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001007 bots = []
1008 cursor = None
1009 limit = 250
1010 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001011 base_url = (
1012 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001013 while True:
1014 url = base_url
1015 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001016 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001017 data = net.url_read_json(url)
1018 if data is None:
1019 print >> sys.stderr, 'Failed to access %s' % options.swarming
1020 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001021 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001022 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001023 if not cursor:
1024 break
1025
maruel77f720b2015-09-15 12:35:22 -07001026 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001027 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001028 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001029 continue
maruel77f720b2015-09-15 12:35:22 -07001030 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001031 continue
1032
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001033 # If the user requested to filter on dimensions, ensure the bot has all the
1034 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001035 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001036 for key, value in options.dimensions:
1037 if key not in dimensions:
1038 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001039 # A bot can have multiple value for a key, for example,
1040 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1041 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001042 if isinstance(dimensions[key], list):
1043 if value not in dimensions[key]:
1044 break
1045 else:
1046 if value != dimensions[key]:
1047 break
1048 else:
maruel77f720b2015-09-15 12:35:22 -07001049 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001050 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001051 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001052 if bot.get('task_id'):
1053 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001054 return 0
1055
1056
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001057@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001058def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001059 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001060
1061 The result can be in multiple part if the execution was sharded. It can
1062 potentially have retries.
1063 """
1064 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001065 parser.add_option(
1066 '-j', '--json',
1067 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001068 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001069 if not args and not options.json:
1070 parser.error('Must specify at least one task id or --json.')
1071 if args and options.json:
1072 parser.error('Only use one of task id or --json.')
1073
1074 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001075 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001076 try:
maruel1ceb3872015-10-14 06:10:44 -07001077 with fs.open(options.json, 'rb') as f:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001078 tasks = sorted(
1079 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1080 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001081 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001082 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001083 else:
1084 valid = frozenset('0123456789abcdef')
1085 if any(not valid.issuperset(task_id) for task_id in args):
1086 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001087
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001088 try:
1089 return collect(
1090 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001091 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001092 options.timeout,
1093 options.decorate,
1094 options.print_status_updates,
1095 options.task_summary_json,
1096 options.task_output_dir)
1097 except Failure:
1098 on_error.report(None)
1099 return 1
1100
1101
maruelbea00862015-09-18 09:55:36 -07001102@subcommand.usage('[filename]')
1103def CMDput_bootstrap(parser, args):
1104 """Uploads a new version of bootstrap.py."""
1105 options, args = parser.parse_args(args)
1106 if len(args) != 1:
1107 parser.error('Must specify file to upload')
1108 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001109 path = unicode(os.path.abspath(args[0]))
1110 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001111 content = f.read().decode('utf-8')
1112 data = net.url_read_json(url, data={'content': content})
1113 print data
1114 return 0
1115
1116
1117@subcommand.usage('[filename]')
1118def CMDput_bot_config(parser, args):
1119 """Uploads a new version of bot_config.py."""
1120 options, args = parser.parse_args(args)
1121 if len(args) != 1:
1122 parser.error('Must specify file to upload')
1123 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001124 path = unicode(os.path.abspath(args[0]))
1125 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001126 content = f.read().decode('utf-8')
1127 data = net.url_read_json(url, data={'content': content})
1128 print data
1129 return 0
1130
1131
maruel77f720b2015-09-15 12:35:22 -07001132@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001133def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001134 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1135 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001136
1137 Examples:
maruel77f720b2015-09-15 12:35:22 -07001138 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001139 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001140
maruel77f720b2015-09-15 12:35:22 -07001141 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001142 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1143
1144 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1145 quoting is important!:
1146 swarming.py query -S server-url.com --limit 10 \\
1147 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001148 """
1149 CHUNK_SIZE = 250
1150
1151 parser.add_option(
1152 '-L', '--limit', type='int', default=200,
1153 help='Limit to enforce on limitless items (like number of tasks); '
1154 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001155 parser.add_option(
1156 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001157 parser.add_option(
1158 '--progress', action='store_true',
1159 help='Prints a dot at each request to show progress')
1160 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001161 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001162 parser.error(
1163 'Must specify only method name and optionally query args properly '
1164 'escaped.')
1165 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001166 url = base_url
1167 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001168 # Check check, change if not working out.
1169 merge_char = '&' if '?' in url else '?'
1170 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001171 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001172 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001173 # TODO(maruel): Do basic diagnostic.
1174 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001175 return 1
1176
1177 # Some items support cursors. Try to get automatically if cursors are needed
1178 # by looking at the 'cursor' items.
1179 while (
1180 data.get('cursor') and
1181 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001182 merge_char = '&' if '?' in base_url else '?'
1183 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001184 if options.limit:
1185 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001186 if options.progress:
1187 sys.stdout.write('.')
1188 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001189 new = net.url_read_json(url)
1190 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001191 if options.progress:
1192 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001193 print >> sys.stderr, 'Failed to access %s' % options.swarming
1194 return 1
maruel81b37132015-10-21 06:42:13 -07001195 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001196 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001197
maruel77f720b2015-09-15 12:35:22 -07001198 if options.progress:
1199 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001200 if options.limit and len(data.get('items', [])) > options.limit:
1201 data['items'] = data['items'][:options.limit]
1202 data.pop('cursor', None)
1203
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001204 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001205 options.json = unicode(os.path.abspath(options.json))
1206 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001207 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001208 try:
maruel77f720b2015-09-15 12:35:22 -07001209 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001210 sys.stdout.write('\n')
1211 except IOError:
1212 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001213 return 0
1214
1215
maruel77f720b2015-09-15 12:35:22 -07001216def CMDquery_list(parser, args):
1217 """Returns list of all the Swarming APIs that can be used with command
1218 'query'.
1219 """
1220 parser.add_option(
1221 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1222 options, args = parser.parse_args(args)
1223 if args:
1224 parser.error('No argument allowed.')
1225
1226 try:
1227 apis = endpoints_api_discovery_apis(options.swarming)
1228 except APIError as e:
1229 parser.error(str(e))
1230 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001231 options.json = unicode(os.path.abspath(options.json))
1232 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001233 json.dump(apis, f)
1234 else:
1235 help_url = (
1236 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1237 options.swarming)
1238 for api_id, api in sorted(apis.iteritems()):
1239 print api_id
1240 print ' ' + api['description']
1241 for resource_name, resource in sorted(api['resources'].iteritems()):
1242 print ''
1243 for method_name, method in sorted(resource['methods'].iteritems()):
1244 # Only list the GET ones.
1245 if method['httpMethod'] != 'GET':
1246 continue
1247 print '- %s.%s: %s' % (
1248 resource_name, method_name, method['path'])
1249 print ' ' + method['description']
1250 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1251 return 0
1252
1253
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001254@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001255def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001256 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001257
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001258 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001259 """
1260 add_trigger_options(parser)
1261 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001262 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001263 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001264 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001265 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001266 tasks = trigger_task_shards(
1267 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001268 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001269 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001270 'Failed to trigger %s(%s): %s' %
1271 (options.task_name, args[0], e.args[0]))
1272 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001273 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001274 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001275 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001276 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001277 task_ids = [
1278 t['task_id']
1279 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1280 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001281 try:
1282 return collect(
1283 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001284 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001285 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001286 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001287 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001288 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001289 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001290 except Failure:
1291 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001292 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001293
1294
maruel18122c62015-10-23 06:31:23 -07001295@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001296def CMDreproduce(parser, args):
1297 """Runs a task locally that was triggered on the server.
1298
1299 This running locally the same commands that have been run on the bot. The data
1300 downloaded will be in a subdirectory named 'work' of the current working
1301 directory.
maruel18122c62015-10-23 06:31:23 -07001302
1303 You can pass further additional arguments to the target command by passing
1304 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001305 """
1306 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001307 extra_args = []
1308 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001309 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001310 if len(args) > 1:
1311 if args[1] == '--':
1312 if len(args) > 2:
1313 extra_args = args[2:]
1314 else:
1315 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001316
maruel77f720b2015-09-15 12:35:22 -07001317 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001318 request = net.url_read_json(url)
1319 if not request:
1320 print >> sys.stderr, 'Failed to retrieve request data for the task'
1321 return 1
1322
maruel12e30012015-10-09 11:55:35 -07001323 workdir = unicode(os.path.abspath('work'))
1324 if not fs.isdir(workdir):
1325 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001326
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001327 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001328 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001329 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001330 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001331 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001332 for i in properties['env']:
1333 key = i['key'].encode('utf-8')
1334 if not i['value']:
1335 env.pop(key, None)
1336 else:
1337 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001338
maruel29ab2fd2015-10-16 11:44:01 -07001339 if properties.get('inputs_ref'):
1340 # Create the tree.
1341 with isolateserver.get_storage(
1342 properties['inputs_ref']['isolatedserver'],
1343 properties['inputs_ref']['namespace']) as storage:
1344 bundle = isolateserver.fetch_isolated(
1345 properties['inputs_ref']['isolated'],
1346 storage,
1347 isolateserver.MemoryCache(file_mode_mask=0700),
1348 workdir,
1349 False)
1350 command = bundle.command
1351 if bundle.relative_cwd:
1352 workdir = os.path.join(workdir, bundle.relative_cwd)
1353 else:
1354 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001355 try:
maruel18122c62015-10-23 06:31:23 -07001356 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001357 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001358 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001359 print >> sys.stderr, str(e)
1360 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001361
1362
maruel0eb1d1b2015-10-02 14:48:21 -07001363@subcommand.usage('bot_id')
1364def CMDterminate(parser, args):
1365 """Tells a bot to gracefully shut itself down as soon as it can.
1366
1367 This is done by completing whatever current task there is then exiting the bot
1368 process.
1369 """
1370 parser.add_option(
1371 '--wait', action='store_true', help='Wait for the bot to terminate')
1372 options, args = parser.parse_args(args)
1373 if len(args) != 1:
1374 parser.error('Please provide the bot id')
1375 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1376 request = net.url_read_json(url, data={})
1377 if not request:
1378 print >> sys.stderr, 'Failed to ask for termination'
1379 return 1
1380 if options.wait:
1381 return collect(
1382 options.swarming, [request['task_id']], 0., False, False, None, None)
1383 return 0
1384
1385
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001386@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001387def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001388 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001389
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001390 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001391 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001392
1393 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001394
1395 Passes all extra arguments provided after '--' as additional command line
1396 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001397 """
1398 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001399 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001400 parser.add_option(
1401 '--dump-json',
1402 metavar='FILE',
1403 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001404 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001405 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001406 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001407 tasks = trigger_task_shards(
1408 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001409 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001410 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001411 tasks_sorted = sorted(
1412 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001413 if options.dump_json:
1414 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001415 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001416 'tasks': tasks,
1417 }
maruel46b015f2015-10-13 18:40:35 -07001418 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001419 print('To collect results, use:')
1420 print(' swarming.py collect -S %s --json %s' %
1421 (options.swarming, options.dump_json))
1422 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001423 print('To collect results, use:')
1424 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001425 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1426 print('Or visit:')
1427 for t in tasks_sorted:
1428 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001429 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001430 except Failure:
1431 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001432 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001433
1434
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001435class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001436 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001437 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001438 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001439 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001440 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001441 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001442 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001443 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001444 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001445 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001446
1447 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001448 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001449 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001450 auth.process_auth_options(self, options)
1451 user = self._process_swarming(options)
1452 if hasattr(options, 'user') and not options.user:
1453 options.user = user
1454 return options, args
1455
1456 def _process_swarming(self, options):
1457 """Processes the --swarming option and aborts if not specified.
1458
1459 Returns the identity as determined by the server.
1460 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001461 if not options.swarming:
1462 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001463 try:
1464 options.swarming = net.fix_url(options.swarming)
1465 except ValueError as e:
1466 self.error('--swarming %s' % e)
1467 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001468 try:
1469 user = auth.ensure_logged_in(options.swarming)
1470 except ValueError as e:
1471 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001472 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001473
1474
1475def main(args):
1476 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001477 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001478
1479
1480if __name__ == '__main__':
1481 fix_encoding.fix_encoding()
1482 tools.disable_buffering()
1483 colorama.init()
1484 sys.exit(main(sys.argv[1:]))