blob: b13e12cd818e7d3b5f1d1301ab195c1c14acbd20 [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
maruelc070e672016-02-22 17:32:57 -080039import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000040
41
42ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050043
44
45class Failure(Exception):
46 """Generic failure."""
47 pass
48
49
50### Isolated file handling.
51
52
maruel77f720b2015-09-15 12:35:22 -070053def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050054 """Archives a .isolated file if needed.
55
56 Returns the file hash to trigger and a bool specifying if it was a file (True)
57 or a hash (False).
58 """
59 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070060 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070061 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050062 if not file_hash:
63 on_error.report('Archival failure %s' % arg)
64 return None, True
65 return file_hash, True
66 elif isolated_format.is_valid_hash(arg, algo):
67 return arg, False
68 else:
69 on_error.report('Invalid hash %s' % arg)
70 return None, False
71
72
73def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050074 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050075
76 Returns:
maruel77f720b2015-09-15 12:35:22 -070077 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050078 """
79 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070080 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050081 if not options.isolated:
82 if '--' in args:
83 index = args.index('--')
84 isolated_cmd_args = args[index+1:]
85 args = args[:index]
86 else:
87 # optparse eats '--' sometimes.
88 isolated_cmd_args = args[1:]
89 args = args[:1]
90 if len(args) != 1:
91 raise ValueError(
92 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
93 'process.')
94 # Old code. To be removed eventually.
95 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070096 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050097 if not options.isolated:
98 raise ValueError('Invalid argument %s' % args[0])
99 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500100 if '--' in args:
101 index = args.index('--')
102 isolated_cmd_args = args[index+1:]
103 if index != 0:
104 raise ValueError('Unexpected arguments.')
105 else:
106 # optparse eats '--' sometimes.
107 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500108
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500109 # If a file name was passed, use its base name of the isolated hash.
110 # Otherwise, use user name as an approximation of a task name.
111 if not options.task_name:
112 if is_file:
113 key = os.path.splitext(os.path.basename(args[0]))[0]
114 else:
115 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500116 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500117 key,
118 '_'.join(
119 '%s=%s' % (k, v)
120 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500121 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500122
maruel77f720b2015-09-15 12:35:22 -0700123 inputs_ref = FilesRef(
124 isolated=options.isolated,
125 isolatedserver=options.isolate_server,
126 namespace=options.namespace)
127 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500128
129
130### Triggering.
131
132
maruel77f720b2015-09-15 12:35:22 -0700133# See ../appengine/swarming/swarming_rpcs.py.
134FilesRef = collections.namedtuple(
135 'FilesRef',
136 [
137 'isolated',
138 'isolatedserver',
139 'namespace',
140 ])
141
142
143# See ../appengine/swarming/swarming_rpcs.py.
144TaskProperties = collections.namedtuple(
145 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500146 [
147 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500148 'dimensions',
149 'env',
maruel77f720b2015-09-15 12:35:22 -0700150 'execution_timeout_secs',
151 'extra_args',
152 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500153 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700154 'inputs_ref',
155 'io_timeout_secs',
156 ])
157
158
159# See ../appengine/swarming/swarming_rpcs.py.
160NewTaskRequest = collections.namedtuple(
161 'NewTaskRequest',
162 [
163 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 'name',
maruel77f720b2015-09-15 12:35:22 -0700165 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500166 'priority',
maruel77f720b2015-09-15 12:35:22 -0700167 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500168 'tags',
169 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170 ])
171
172
maruel77f720b2015-09-15 12:35:22 -0700173def namedtuple_to_dict(value):
174 """Recursively converts a namedtuple to a dict."""
175 out = dict(value._asdict())
176 for k, v in out.iteritems():
177 if hasattr(v, '_asdict'):
178 out[k] = namedtuple_to_dict(v)
179 return out
180
181
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500182def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800183 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700184
185 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500186 """
maruel77f720b2015-09-15 12:35:22 -0700187 out = namedtuple_to_dict(task_request)
188 # Maps are not supported until protobuf v3.
189 out['properties']['dimensions'] = [
190 {'key': k, 'value': v}
191 for k, v in out['properties']['dimensions'].iteritems()
192 ]
193 out['properties']['dimensions'].sort(key=lambda x: x['key'])
194 out['properties']['env'] = [
195 {'key': k, 'value': v}
196 for k, v in out['properties']['env'].iteritems()
197 ]
198 out['properties']['env'].sort(key=lambda x: x['key'])
199 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500200
201
maruel77f720b2015-09-15 12:35:22 -0700202def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500203 """Triggers a request on the Swarming server and returns the json data.
204
205 It's the low-level function.
206
207 Returns:
208 {
209 'request': {
210 'created_ts': u'2010-01-02 03:04:05',
211 'name': ..
212 },
213 'task_id': '12300',
214 }
215 """
216 logging.info('Triggering: %s', raw_request['name'])
217
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500218 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700219 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500220 if not result:
221 on_error.report('Failed to trigger task %s' % raw_request['name'])
222 return None
maruele557bce2015-11-17 09:01:27 -0800223 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800224 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800225 msg = 'Failed to trigger task %s' % raw_request['name']
226 if result['error'].get('errors'):
227 for err in result['error']['errors']:
228 if err.get('message'):
229 msg += '\nMessage: %s' % err['message']
230 if err.get('debugInfo'):
231 msg += '\nDebug info:\n%s' % err['debugInfo']
232 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800233 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800234
235 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800236 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500237 return result
238
239
240def setup_googletest(env, shards, index):
241 """Sets googletest specific environment variables."""
242 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700243 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
244 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
245 env = env[:]
246 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
247 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 return env
249
250
251def trigger_task_shards(swarming, task_request, shards):
252 """Triggers one or many subtasks of a sharded task.
253
254 Returns:
255 Dict with task details, returned to caller as part of --dump-json output.
256 None in case of failure.
257 """
258 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700259 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500260 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700261 req['properties']['env'] = setup_googletest(
262 req['properties']['env'], shards, index)
263 req['name'] += ':%s:%s' % (index, shards)
264 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500265
266 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500267 tasks = {}
268 priority_warning = False
269 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700270 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500271 if not task:
272 break
273 logging.info('Request result: %s', task)
274 if (not priority_warning and
275 task['request']['priority'] != task_request.priority):
276 priority_warning = True
277 print >> sys.stderr, (
278 'Priority was reset to %s' % task['request']['priority'])
279 tasks[request['name']] = {
280 'shard_index': index,
281 'task_id': task['task_id'],
282 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
283 }
284
285 # Some shards weren't triggered. Abort everything.
286 if len(tasks) != len(requests):
287 if tasks:
288 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
289 len(tasks), len(requests))
290 for task_dict in tasks.itervalues():
291 abort_task(swarming, task_dict['task_id'])
292 return None
293
294 return tasks
295
296
297### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000298
299
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700300# How often to print status updates to stdout in 'collect'.
301STATUS_UPDATE_INTERVAL = 15 * 60.
302
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400303
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400304class State(object):
305 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000306
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400307 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
308 values are part of the API so if they change, the API changed.
309
310 It's in fact an enum. Values should be in decreasing order of importance.
311 """
312 RUNNING = 0x10
313 PENDING = 0x20
314 EXPIRED = 0x30
315 TIMED_OUT = 0x40
316 BOT_DIED = 0x50
317 CANCELED = 0x60
318 COMPLETED = 0x70
319
maruel77f720b2015-09-15 12:35:22 -0700320 STATES = (
321 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
322 'COMPLETED')
323 STATES_RUNNING = ('RUNNING', 'PENDING')
324 STATES_NOT_RUNNING = (
325 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
326 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
327 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400328
329 _NAMES = {
330 RUNNING: 'Running',
331 PENDING: 'Pending',
332 EXPIRED: 'Expired',
333 TIMED_OUT: 'Execution timed out',
334 BOT_DIED: 'Bot died',
335 CANCELED: 'User canceled',
336 COMPLETED: 'Completed',
337 }
338
maruel77f720b2015-09-15 12:35:22 -0700339 _ENUMS = {
340 'RUNNING': RUNNING,
341 'PENDING': PENDING,
342 'EXPIRED': EXPIRED,
343 'TIMED_OUT': TIMED_OUT,
344 'BOT_DIED': BOT_DIED,
345 'CANCELED': CANCELED,
346 'COMPLETED': COMPLETED,
347 }
348
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400349 @classmethod
350 def to_string(cls, state):
351 """Returns a user-readable string representing a State."""
352 if state not in cls._NAMES:
353 raise ValueError('Invalid state %s' % state)
354 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000355
maruel77f720b2015-09-15 12:35:22 -0700356 @classmethod
357 def from_enum(cls, state):
358 """Returns int value based on the string."""
359 if state not in cls._ENUMS:
360 raise ValueError('Invalid state %s' % state)
361 return cls._ENUMS[state]
362
maruel@chromium.org0437a732013-08-27 16:05:52 +0000363
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700365 """Assembles task execution summary (for --task-summary-json output).
366
367 Optionally fetches task outputs from isolate server to local disk (used when
368 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369
370 This object is shared among multiple threads running 'retrieve_results'
371 function, in particular they call 'process_shard_result' method in parallel.
372 """
373
maruel0eb1d1b2015-10-02 14:48:21 -0700374 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700375 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
376
377 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700378 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700379 shard_count: expected number of task shards.
380 """
maruel12e30012015-10-09 11:55:35 -0700381 self.task_output_dir = (
382 unicode(os.path.abspath(task_output_dir))
383 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 self.shard_count = shard_count
385
386 self._lock = threading.Lock()
387 self._per_shard_results = {}
388 self._storage = None
389
maruel12e30012015-10-09 11:55:35 -0700390 if self.task_output_dir and not fs.isdir(self.task_output_dir):
391 fs.makedirs(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700392
Vadim Shtayurab450c602014-05-12 19:23:25 -0700393 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700394 """Stores results of a single task shard, fetches output files if necessary.
395
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400396 Modifies |result| in place.
397
maruel77f720b2015-09-15 12:35:22 -0700398 shard_index is 0-based.
399
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400 Called concurrently from multiple threads.
401 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700403 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700404 if shard_index < 0 or shard_index >= self.shard_count:
405 logging.warning(
406 'Shard index %d is outside of expected range: [0; %d]',
407 shard_index, self.shard_count - 1)
408 return
409
maruel77f720b2015-09-15 12:35:22 -0700410 if result.get('outputs_ref'):
411 ref = result['outputs_ref']
412 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
413 ref['isolatedserver'],
414 urllib.urlencode(
415 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400416
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700417 # Store result dict of that shard, ignore results we've already seen.
418 with self._lock:
419 if shard_index in self._per_shard_results:
420 logging.warning('Ignoring duplicate shard index %d', shard_index)
421 return
422 self._per_shard_results[shard_index] = result
423
424 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700425 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400426 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700427 result['outputs_ref']['isolatedserver'],
428 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400429 if storage:
430 # Output files are supposed to be small and they are not reused across
431 # tasks. So use MemoryCache for them instead of on-disk cache. Make
432 # files writable, so that calling script can delete them.
433 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700434 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400435 storage,
436 isolateserver.MemoryCache(file_mode_mask=0700),
437 os.path.join(self.task_output_dir, str(shard_index)),
438 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439
440 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700441 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442 with self._lock:
443 # Write an array of shard results with None for missing shards.
444 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 'shards': [
446 self._per_shard_results.get(i) for i in xrange(self.shard_count)
447 ],
448 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700449 # Write summary.json to task_output_dir as well.
450 if self.task_output_dir:
451 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700452 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700453 summary,
454 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700455 if self._storage:
456 self._storage.close()
457 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700458 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700459
460 def _get_storage(self, isolate_server, namespace):
461 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700462 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700463 with self._lock:
464 if not self._storage:
465 self._storage = isolateserver.get_storage(isolate_server, namespace)
466 else:
467 # Shards must all use exact same isolate server and namespace.
468 if self._storage.location != isolate_server:
469 logging.error(
470 'Task shards are using multiple isolate servers: %s and %s',
471 self._storage.location, isolate_server)
472 return None
473 if self._storage.namespace != namespace:
474 logging.error(
475 'Task shards are using multiple namespaces: %s and %s',
476 self._storage.namespace, namespace)
477 return None
478 return self._storage
479
480
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500481def now():
482 """Exists so it can be mocked easily."""
483 return time.time()
484
485
maruel77f720b2015-09-15 12:35:22 -0700486def parse_time(value):
487 """Converts serialized time from the API to datetime.datetime."""
488 # When microseconds are 0, the '.123456' suffix is elided. This means the
489 # serialized format is not consistent, which confuses the hell out of python.
490 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
491 try:
492 return datetime.datetime.strptime(value, fmt)
493 except ValueError:
494 pass
495 raise ValueError('Failed to parse %s' % value)
496
497
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700498def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400499 base_url, shard_index, task_id, timeout, should_stop, output_collector):
500 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700501
Vadim Shtayurab450c602014-05-12 19:23:25 -0700502 Returns:
503 <result dict> on success.
504 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700505 """
maruel71c61c82016-02-22 06:52:05 -0800506 assert timeout is None or isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700507 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
508 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700509 started = now()
510 deadline = started + timeout if timeout else None
511 attempt = 0
512
513 while not should_stop.is_set():
514 attempt += 1
515
516 # Waiting for too long -> give up.
517 current_time = now()
518 if deadline and current_time >= deadline:
519 logging.error('retrieve_results(%s) timed out on attempt %d',
520 base_url, attempt)
521 return None
522
523 # Do not spin too fast. Spin faster at the beginning though.
524 # Start with 1 sec delay and for each 30 sec of waiting add another second
525 # of delay, until hitting 15 sec ceiling.
526 if attempt > 1:
527 max_delay = min(15, 1 + (current_time - started) / 30.0)
528 delay = min(max_delay, deadline - current_time) if deadline else max_delay
529 if delay > 0:
530 logging.debug('Waiting %.1f sec before retrying', delay)
531 should_stop.wait(delay)
532 if should_stop.is_set():
533 return None
534
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400535 # Disable internal retries in net.url_read_json, since we are doing retries
536 # ourselves.
537 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700538 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
539 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400540 result = net.url_read_json(result_url, retry_50x=False)
541 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400542 continue
maruel77f720b2015-09-15 12:35:22 -0700543
maruelbf53e042015-12-01 15:00:51 -0800544 if result.get('error'):
545 # An error occurred.
546 if result['error'].get('errors'):
547 for err in result['error']['errors']:
548 logging.warning(
549 'Error while reading task: %s; %s',
550 err.get('message'), err.get('debugInfo'))
551 elif result['error'].get('message'):
552 logging.warning(
553 'Error while reading task: %s', result['error']['message'])
554 continue
555
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400556 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700557 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400558 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700559 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700560 # Record the result, try to fetch attached output files (if any).
561 if output_collector:
562 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700563 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700564 if result.get('internal_failure'):
565 logging.error('Internal error!')
566 elif result['state'] == 'BOT_DIED':
567 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700568 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000569
570
maruel77f720b2015-09-15 12:35:22 -0700571def convert_to_old_format(result):
572 """Converts the task result data from Endpoints API format to old API format
573 for compatibility.
574
575 This goes into the file generated as --task-summary-json.
576 """
577 # Sets default.
578 result.setdefault('abandoned_ts', None)
579 result.setdefault('bot_id', None)
580 result.setdefault('bot_version', None)
581 result.setdefault('children_task_ids', [])
582 result.setdefault('completed_ts', None)
583 result.setdefault('cost_saved_usd', None)
584 result.setdefault('costs_usd', None)
585 result.setdefault('deduped_from', None)
586 result.setdefault('name', None)
587 result.setdefault('outputs_ref', None)
588 result.setdefault('properties_hash', None)
589 result.setdefault('server_versions', None)
590 result.setdefault('started_ts', None)
591 result.setdefault('tags', None)
592 result.setdefault('user', None)
593
594 # Convertion back to old API.
595 duration = result.pop('duration', None)
596 result['durations'] = [duration] if duration else []
597 exit_code = result.pop('exit_code', None)
598 result['exit_codes'] = [int(exit_code)] if exit_code else []
599 result['id'] = result.pop('task_id')
600 result['isolated_out'] = result.get('outputs_ref', None)
601 output = result.pop('output', None)
602 result['outputs'] = [output] if output else []
603 # properties_hash
604 # server_version
605 # Endpoints result 'state' as string. For compatibility with old code, convert
606 # to int.
607 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700608 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700609 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700610 if 'bot_dimensions' in result:
611 result['bot_dimensions'] = {
612 i['key']: i['value'] for i in result['bot_dimensions']
613 }
614 else:
615 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700616
617
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700618def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400619 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
620 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500621 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000622
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700623 Duplicate shards are ignored. Shards are yielded in order of completion.
624 Timed out shards are NOT yielded at all. Caller can compare number of yielded
625 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000626
627 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500628 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 +0000629 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500630
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700631 output_collector is an optional instance of TaskOutputCollector that will be
632 used to fetch files produced by a task from isolate server to the local disk.
633
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500634 Yields:
635 (index, result). In particular, 'result' is defined as the
636 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000637 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000638 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400639 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700640 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700641 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700642
maruel@chromium.org0437a732013-08-27 16:05:52 +0000643 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
644 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700645 # Adds a task to the thread pool to call 'retrieve_results' and return
646 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400647 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700648 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000649 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400650 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
651 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652
653 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400654 for shard_index, task_id in enumerate(task_ids):
655 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700656
657 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400658 shards_remaining = range(len(task_ids))
659 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700661 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700662 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700663 shard_index, result = results_channel.pull(
664 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700665 except threading_utils.TaskChannel.Timeout:
666 if print_status_updates:
667 print(
668 'Waiting for results from the following shards: %s' %
669 ', '.join(map(str, shards_remaining)))
670 sys.stdout.flush()
671 continue
672 except Exception:
673 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700674
675 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700676 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500678 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700680
Vadim Shtayurab450c602014-05-12 19:23:25 -0700681 # Yield back results to the caller.
682 assert shard_index in shards_remaining
683 shards_remaining.remove(shard_index)
684 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700685
maruel@chromium.org0437a732013-08-27 16:05:52 +0000686 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700687 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000688 should_stop.set()
689
690
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400691def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000692 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700693 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400694 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700695 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
696 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400697 else:
698 pending = 'N/A'
699
maruel77f720b2015-09-15 12:35:22 -0700700 if metadata.get('duration') is not None:
701 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400702 else:
703 duration = 'N/A'
704
maruel77f720b2015-09-15 12:35:22 -0700705 if metadata.get('exit_code') is not None:
706 # Integers are encoded as string to not loose precision.
707 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400708 else:
709 exit_code = 'N/A'
710
711 bot_id = metadata.get('bot_id') or 'N/A'
712
maruel77f720b2015-09-15 12:35:22 -0700713 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400714 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400715 tag_footer = (
716 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
717 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400718
719 tag_len = max(len(tag_header), len(tag_footer))
720 dash_pad = '+-%s-+\n' % ('-' * tag_len)
721 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
722 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
723
724 header = dash_pad + tag_header + dash_pad
725 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700726 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400727 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000728
729
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700730def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700731 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400732 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700733 """Retrieves results of a Swarming task.
734
735 Returns:
736 process exit code that should be returned to the user.
737 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700738 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700739 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700740
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700741 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700742 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400743 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700744 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400745 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400746 swarming, task_ids, timeout, None, print_status_updates,
747 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700748 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700749
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400750 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700751 shard_exit_code = metadata.get('exit_code')
752 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700753 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700754 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700755 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400756 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700757 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700758
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700759 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400760 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400761 if len(seen_shards) < len(task_ids):
762 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700763 else:
maruel77f720b2015-09-15 12:35:22 -0700764 print('%s: %s %s' % (
765 metadata.get('bot_id', 'N/A'),
766 metadata['task_id'],
767 shard_exit_code))
768 if metadata['output']:
769 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400770 if output:
771 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700772 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700773 summary = output_collector.finalize()
774 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700775 # TODO(maruel): Make this optional.
776 for i in summary['shards']:
777 if i:
778 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700779 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700780
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400781 if decorate and total_duration:
782 print('Total duration: %.1fs' % total_duration)
783
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400784 if len(seen_shards) != len(task_ids):
785 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700786 print >> sys.stderr, ('Results from some shards are missing: %s' %
787 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700788 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700789
maruela5490782015-09-30 10:56:59 -0700790 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000791
792
maruel77f720b2015-09-15 12:35:22 -0700793### API management.
794
795
796class APIError(Exception):
797 pass
798
799
800def endpoints_api_discovery_apis(host):
801 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
802 the APIs exposed by a host.
803
804 https://developers.google.com/discovery/v1/reference/apis/list
805 """
806 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
807 if data is None:
808 raise APIError('Failed to discover APIs on %s' % host)
809 out = {}
810 for api in data['items']:
811 if api['id'] == 'discovery:v1':
812 continue
813 # URL is of the following form:
814 # url = host + (
815 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
816 api_data = net.url_read_json(api['discoveryRestUrl'])
817 if api_data is None:
818 raise APIError('Failed to discover %s on %s' % (api['id'], host))
819 out[api['id']] = api_data
820 return out
821
822
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500823### Commands.
824
825
826def abort_task(_swarming, _manifest):
827 """Given a task manifest that was triggered, aborts its execution."""
828 # TODO(vadimsh): No supported by the server yet.
829
830
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400831def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400832 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500833 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500834 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500835 dest='dimensions', metavar='FOO bar',
836 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500837 parser.add_option_group(parser.filter_group)
838
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400839
Vadim Shtayurab450c602014-05-12 19:23:25 -0700840def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400841 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700842 parser.sharding_group.add_option(
843 '--shards', type='int', default=1,
844 help='Number of shards to trigger and collect.')
845 parser.add_option_group(parser.sharding_group)
846
847
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400848def add_trigger_options(parser):
849 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500850 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400851 add_filter_options(parser)
852
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400853 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500854 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500855 '-s', '--isolated',
856 help='Hash of the .isolated to grab from the isolate server')
857 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500858 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700859 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500860 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500861 '--priority', type='int', default=100,
862 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500863 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500864 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400865 help='Display name of the task. Defaults to '
866 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
867 'isolated file is provided, if a hash is provided, it defaults to '
868 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400869 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400870 '--tags', action='append', default=[],
871 help='Tags to assign to the task.')
872 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500873 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400874 help='User associated with the task. Defaults to authenticated user on '
875 'the server.')
876 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400877 '--idempotent', action='store_true', default=False,
878 help='When set, the server will actively try to find a previous task '
879 'with the same parameter and return this result instead if possible')
880 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400881 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400882 help='Seconds to allow the task to be pending for a bot to run before '
883 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400884 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400885 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400886 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400887 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400888 '--hard-timeout', type='int', default=60*60,
889 help='Seconds to allow the task to complete.')
890 parser.task_group.add_option(
891 '--io-timeout', type='int', default=20*60,
892 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500893 parser.task_group.add_option(
894 '--raw-cmd', action='store_true', default=False,
895 help='When set, the command after -- is used as-is without run_isolated. '
896 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500897 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000898
899
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500900def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500901 """Processes trigger options and uploads files to isolate server if necessary.
902 """
903 options.dimensions = dict(options.dimensions)
904 options.env = dict(options.env)
905
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500906 if not options.dimensions:
907 parser.error('Please at least specify one --dimension')
908 if options.raw_cmd:
909 if not args:
910 parser.error(
911 'Arguments with --raw-cmd should be passed after -- as command '
912 'delimiter.')
913 if options.isolate_server:
914 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
915
916 command = args
917 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500918 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500919 options.user,
920 '_'.join(
921 '%s=%s' % (k, v)
922 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700923 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500924 else:
925 isolateserver.process_isolate_server_options(parser, options, False)
926 try:
maruel77f720b2015-09-15 12:35:22 -0700927 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500928 except ValueError as e:
929 parser.error(str(e))
930
maruel77f720b2015-09-15 12:35:22 -0700931 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
932 # actual command to run.
933 properties = TaskProperties(
934 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500935 dimensions=options.dimensions,
936 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700937 execution_timeout_secs=options.hard_timeout,
938 extra_args=command if inputs_ref else None,
939 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500940 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700941 inputs_ref=inputs_ref,
942 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700943 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
944 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700945 return NewTaskRequest(
946 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500947 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700948 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500949 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700950 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500951 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700952 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000953
954
955def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500956 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -0800957 '-t', '--timeout', type='float',
958 help='Timeout to wait for result, set to 0 for no timeout; default to no '
959 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500960 parser.group_logging.add_option(
961 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700962 parser.group_logging.add_option(
963 '--print-status-updates', action='store_true',
964 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400965 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700966 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700967 '--task-summary-json',
968 metavar='FILE',
969 help='Dump a summary of task results to this file as json. It contains '
970 'only shards statuses as know to server directly. Any output files '
971 'emitted by the task can be collected by using --task-output-dir')
972 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700973 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700974 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700975 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700976 'directory contains per-shard directory with output files produced '
977 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700978 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000979
980
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400981@subcommand.usage('bots...')
982def CMDbot_delete(parser, args):
983 """Forcibly deletes bots from the Swarming server."""
984 parser.add_option(
985 '-f', '--force', action='store_true',
986 help='Do not prompt for confirmation')
987 options, args = parser.parse_args(args)
988 if not args:
989 parser.error('Please specific bots to delete')
990
991 bots = sorted(args)
992 if not options.force:
993 print('Delete the following bots?')
994 for bot in bots:
995 print(' %s' % bot)
996 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
997 print('Goodbye.')
998 return 1
999
1000 result = 0
1001 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001002 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1003 if net.url_read_json(url, data={}, method='POST') is None:
1004 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001005 result = 1
1006 return result
1007
1008
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001009def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001010 """Returns information about the bots connected to the Swarming server."""
1011 add_filter_options(parser)
1012 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001013 '--dead-only', action='store_true',
1014 help='Only print dead bots, useful to reap them and reimage broken bots')
1015 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001016 '-k', '--keep-dead', action='store_true',
1017 help='Do not filter out dead bots')
1018 parser.filter_group.add_option(
1019 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001020 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001021 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001022
1023 if options.keep_dead and options.dead_only:
1024 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001025
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001026 bots = []
1027 cursor = None
1028 limit = 250
1029 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001030 base_url = (
1031 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001032 while True:
1033 url = base_url
1034 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001035 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001036 data = net.url_read_json(url)
1037 if data is None:
1038 print >> sys.stderr, 'Failed to access %s' % options.swarming
1039 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001040 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001041 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001042 if not cursor:
1043 break
1044
maruel77f720b2015-09-15 12:35:22 -07001045 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001046 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001047 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001048 continue
maruel77f720b2015-09-15 12:35:22 -07001049 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001050 continue
1051
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001052 # If the user requested to filter on dimensions, ensure the bot has all the
1053 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001054 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001055 for key, value in options.dimensions:
1056 if key not in dimensions:
1057 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001058 # A bot can have multiple value for a key, for example,
1059 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1060 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001061 if isinstance(dimensions[key], list):
1062 if value not in dimensions[key]:
1063 break
1064 else:
1065 if value != dimensions[key]:
1066 break
1067 else:
maruel77f720b2015-09-15 12:35:22 -07001068 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001069 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001070 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001071 if bot.get('task_id'):
1072 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001073 return 0
1074
1075
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001076@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001077def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001078 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001079
1080 The result can be in multiple part if the execution was sharded. It can
1081 potentially have retries.
1082 """
1083 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001084 parser.add_option(
1085 '-j', '--json',
1086 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001087 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001088 if not args and not options.json:
1089 parser.error('Must specify at least one task id or --json.')
1090 if args and options.json:
1091 parser.error('Only use one of task id or --json.')
1092
1093 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001094 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001095 try:
maruel1ceb3872015-10-14 06:10:44 -07001096 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001097 data = json.load(f)
1098 except (IOError, ValueError):
1099 parser.error('Failed to open %s' % options.json)
1100 try:
1101 tasks = sorted(
1102 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1103 args = [t['task_id'] for t in tasks]
1104 except (KeyError, TypeError):
1105 parser.error('Failed to process %s' % options.json)
1106 if options.timeout is None:
1107 options.timeout = (
1108 data['request']['properties']['execution_timeout_secs'] +
1109 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001110 else:
1111 valid = frozenset('0123456789abcdef')
1112 if any(not valid.issuperset(task_id) for task_id in args):
1113 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001114
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001115 try:
1116 return collect(
1117 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001118 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001119 options.timeout,
1120 options.decorate,
1121 options.print_status_updates,
1122 options.task_summary_json,
1123 options.task_output_dir)
1124 except Failure:
1125 on_error.report(None)
1126 return 1
1127
1128
maruelbea00862015-09-18 09:55:36 -07001129@subcommand.usage('[filename]')
1130def CMDput_bootstrap(parser, args):
1131 """Uploads a new version of bootstrap.py."""
1132 options, args = parser.parse_args(args)
1133 if len(args) != 1:
1134 parser.error('Must specify file to upload')
1135 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001136 path = unicode(os.path.abspath(args[0]))
1137 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001138 content = f.read().decode('utf-8')
1139 data = net.url_read_json(url, data={'content': content})
1140 print data
1141 return 0
1142
1143
1144@subcommand.usage('[filename]')
1145def CMDput_bot_config(parser, args):
1146 """Uploads a new version of bot_config.py."""
1147 options, args = parser.parse_args(args)
1148 if len(args) != 1:
1149 parser.error('Must specify file to upload')
1150 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001151 path = unicode(os.path.abspath(args[0]))
1152 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001153 content = f.read().decode('utf-8')
1154 data = net.url_read_json(url, data={'content': content})
1155 print data
1156 return 0
1157
1158
maruel77f720b2015-09-15 12:35:22 -07001159@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001160def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001161 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1162 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001163
1164 Examples:
maruel77f720b2015-09-15 12:35:22 -07001165 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001166 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001167
maruel77f720b2015-09-15 12:35:22 -07001168 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001169 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1170
1171 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1172 quoting is important!:
1173 swarming.py query -S server-url.com --limit 10 \\
1174 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001175 """
1176 CHUNK_SIZE = 250
1177
1178 parser.add_option(
1179 '-L', '--limit', type='int', default=200,
1180 help='Limit to enforce on limitless items (like number of tasks); '
1181 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001182 parser.add_option(
1183 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001184 parser.add_option(
1185 '--progress', action='store_true',
1186 help='Prints a dot at each request to show progress')
1187 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001188 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001189 parser.error(
1190 'Must specify only method name and optionally query args properly '
1191 'escaped.')
1192 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001193 url = base_url
1194 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001195 # Check check, change if not working out.
1196 merge_char = '&' if '?' in url else '?'
1197 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001198 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001199 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001200 # TODO(maruel): Do basic diagnostic.
1201 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001202 return 1
1203
1204 # Some items support cursors. Try to get automatically if cursors are needed
1205 # by looking at the 'cursor' items.
1206 while (
1207 data.get('cursor') and
1208 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001209 merge_char = '&' if '?' in base_url else '?'
1210 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001211 if options.limit:
1212 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001213 if options.progress:
1214 sys.stdout.write('.')
1215 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001216 new = net.url_read_json(url)
1217 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001218 if options.progress:
1219 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001220 print >> sys.stderr, 'Failed to access %s' % options.swarming
1221 return 1
maruel81b37132015-10-21 06:42:13 -07001222 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001223 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001224
maruel77f720b2015-09-15 12:35:22 -07001225 if options.progress:
1226 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001227 if options.limit and len(data.get('items', [])) > options.limit:
1228 data['items'] = data['items'][:options.limit]
1229 data.pop('cursor', None)
1230
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001231 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001232 options.json = unicode(os.path.abspath(options.json))
1233 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001234 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001235 try:
maruel77f720b2015-09-15 12:35:22 -07001236 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001237 sys.stdout.write('\n')
1238 except IOError:
1239 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001240 return 0
1241
1242
maruel77f720b2015-09-15 12:35:22 -07001243def CMDquery_list(parser, args):
1244 """Returns list of all the Swarming APIs that can be used with command
1245 'query'.
1246 """
1247 parser.add_option(
1248 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1249 options, args = parser.parse_args(args)
1250 if args:
1251 parser.error('No argument allowed.')
1252
1253 try:
1254 apis = endpoints_api_discovery_apis(options.swarming)
1255 except APIError as e:
1256 parser.error(str(e))
1257 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001258 options.json = unicode(os.path.abspath(options.json))
1259 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001260 json.dump(apis, f)
1261 else:
1262 help_url = (
1263 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1264 options.swarming)
1265 for api_id, api in sorted(apis.iteritems()):
1266 print api_id
1267 print ' ' + api['description']
1268 for resource_name, resource in sorted(api['resources'].iteritems()):
1269 print ''
1270 for method_name, method in sorted(resource['methods'].iteritems()):
1271 # Only list the GET ones.
1272 if method['httpMethod'] != 'GET':
1273 continue
1274 print '- %s.%s: %s' % (
1275 resource_name, method_name, method['path'])
1276 print ' ' + method['description']
1277 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1278 return 0
1279
1280
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001281@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001282def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001283 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001284
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001285 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001286 """
1287 add_trigger_options(parser)
1288 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001289 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001290 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001291 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001292 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001293 tasks = trigger_task_shards(
1294 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001295 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001296 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001297 'Failed to trigger %s(%s): %s' %
1298 (options.task_name, args[0], e.args[0]))
1299 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001300 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001301 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001302 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001303 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001304 task_ids = [
1305 t['task_id']
1306 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1307 ]
maruel71c61c82016-02-22 06:52:05 -08001308 if options.timeout is None:
1309 options.timeout = (
1310 task_request.properties.execution_timeout_secs +
1311 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001312 try:
1313 return collect(
1314 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001315 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001316 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001317 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001318 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001319 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001320 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001321 except Failure:
1322 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001323 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001324
1325
maruel18122c62015-10-23 06:31:23 -07001326@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001327def CMDreproduce(parser, args):
1328 """Runs a task locally that was triggered on the server.
1329
1330 This running locally the same commands that have been run on the bot. The data
1331 downloaded will be in a subdirectory named 'work' of the current working
1332 directory.
maruel18122c62015-10-23 06:31:23 -07001333
1334 You can pass further additional arguments to the target command by passing
1335 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001336 """
maruelc070e672016-02-22 17:32:57 -08001337 parser.add_option(
1338 '--output-dir', metavar='DIR', default='',
1339 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001340 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001341 extra_args = []
1342 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001343 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001344 if len(args) > 1:
1345 if args[1] == '--':
1346 if len(args) > 2:
1347 extra_args = args[2:]
1348 else:
1349 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001350
maruel77f720b2015-09-15 12:35:22 -07001351 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001352 request = net.url_read_json(url)
1353 if not request:
1354 print >> sys.stderr, 'Failed to retrieve request data for the task'
1355 return 1
1356
maruel12e30012015-10-09 11:55:35 -07001357 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001358 if fs.isdir(workdir):
1359 parser.error('Please delete the directory \'work\' first')
1360 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001361
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001362 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001363 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001364 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001365 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001366 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001367 for i in properties['env']:
1368 key = i['key'].encode('utf-8')
1369 if not i['value']:
1370 env.pop(key, None)
1371 else:
1372 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001373
maruel29ab2fd2015-10-16 11:44:01 -07001374 if properties.get('inputs_ref'):
1375 # Create the tree.
1376 with isolateserver.get_storage(
1377 properties['inputs_ref']['isolatedserver'],
1378 properties['inputs_ref']['namespace']) as storage:
1379 bundle = isolateserver.fetch_isolated(
1380 properties['inputs_ref']['isolated'],
1381 storage,
1382 isolateserver.MemoryCache(file_mode_mask=0700),
1383 workdir,
1384 False)
1385 command = bundle.command
1386 if bundle.relative_cwd:
1387 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001388 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001389 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1390 new_command = run_isolated.process_command(command, options.output_dir)
1391 if not options.output_dir and new_command != command:
1392 parser.error('The task has outputs, you must use --output-dir')
1393 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001394 else:
1395 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001396 try:
maruel18122c62015-10-23 06:31:23 -07001397 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001398 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001399 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001400 print >> sys.stderr, str(e)
1401 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001402
1403
maruel0eb1d1b2015-10-02 14:48:21 -07001404@subcommand.usage('bot_id')
1405def CMDterminate(parser, args):
1406 """Tells a bot to gracefully shut itself down as soon as it can.
1407
1408 This is done by completing whatever current task there is then exiting the bot
1409 process.
1410 """
1411 parser.add_option(
1412 '--wait', action='store_true', help='Wait for the bot to terminate')
1413 options, args = parser.parse_args(args)
1414 if len(args) != 1:
1415 parser.error('Please provide the bot id')
1416 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1417 request = net.url_read_json(url, data={})
1418 if not request:
1419 print >> sys.stderr, 'Failed to ask for termination'
1420 return 1
1421 if options.wait:
1422 return collect(
1423 options.swarming, [request['task_id']], 0., False, False, None, None)
1424 return 0
1425
1426
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001427@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001428def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001429 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001430
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001431 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001432 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001433
1434 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001435
1436 Passes all extra arguments provided after '--' as additional command line
1437 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001438 """
1439 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001440 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001441 parser.add_option(
1442 '--dump-json',
1443 metavar='FILE',
1444 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001445 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001446 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001447 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001448 tasks = trigger_task_shards(
1449 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001450 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001451 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001452 tasks_sorted = sorted(
1453 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001454 if options.dump_json:
1455 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001456 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001457 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001458 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001459 }
maruel46b015f2015-10-13 18:40:35 -07001460 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001461 print('To collect results, use:')
1462 print(' swarming.py collect -S %s --json %s' %
1463 (options.swarming, options.dump_json))
1464 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001465 print('To collect results, use:')
1466 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001467 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1468 print('Or visit:')
1469 for t in tasks_sorted:
1470 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001471 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001472 except Failure:
1473 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001474 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001475
1476
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001477class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001478 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001479 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001480 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001481 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001482 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001483 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001484 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001485 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001486 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001487 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001488
1489 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001490 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001491 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001492 auth.process_auth_options(self, options)
1493 user = self._process_swarming(options)
1494 if hasattr(options, 'user') and not options.user:
1495 options.user = user
1496 return options, args
1497
1498 def _process_swarming(self, options):
1499 """Processes the --swarming option and aborts if not specified.
1500
1501 Returns the identity as determined by the server.
1502 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001503 if not options.swarming:
1504 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001505 try:
1506 options.swarming = net.fix_url(options.swarming)
1507 except ValueError as e:
1508 self.error('--swarming %s' % e)
1509 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001510 try:
1511 user = auth.ensure_logged_in(options.swarming)
1512 except ValueError as e:
1513 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001514 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001515
1516
1517def main(args):
1518 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001519 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001520
1521
1522if __name__ == '__main__':
1523 fix_encoding.fix_encoding()
1524 tools.disable_buffering()
1525 colorama.init()
1526 sys.exit(main(sys.argv[1:]))