blob: b5fb198be61049771690209df9cfac822c2ce2b8 [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that 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
maruel9531ce02016-04-13 06:11:23 -07008__version__ = '0.8.5'
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(
nodir152cba62016-05-12 16:08:56 -0700124 isolated=options.isolated,
125 isolatedserver=options.isolate_server,
126 namespace=options.namespace)
maruel77f720b2015-09-15 12:35:22 -0700127 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
nodire5028a92016-04-29 14:38:21 -0700390 if self.task_output_dir:
391 file_path.ensure_tree(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),
maruelb8d88d12016-04-08 12:54:01 -0700437 os.path.join(self.task_output_dir, str(shard_index)))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438
439 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441 with self._lock:
442 # Write an array of shard results with None for missing shards.
443 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444 'shards': [
445 self._per_shard_results.get(i) for i in xrange(self.shard_count)
446 ],
447 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700448 # Write summary.json to task_output_dir as well.
449 if self.task_output_dir:
450 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700451 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700452 summary,
453 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 if self._storage:
455 self._storage.close()
456 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700457 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700458
459 def _get_storage(self, isolate_server, namespace):
460 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700461 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700462 with self._lock:
463 if not self._storage:
464 self._storage = isolateserver.get_storage(isolate_server, namespace)
465 else:
466 # Shards must all use exact same isolate server and namespace.
467 if self._storage.location != isolate_server:
468 logging.error(
469 'Task shards are using multiple isolate servers: %s and %s',
470 self._storage.location, isolate_server)
471 return None
472 if self._storage.namespace != namespace:
473 logging.error(
474 'Task shards are using multiple namespaces: %s and %s',
475 self._storage.namespace, namespace)
476 return None
477 return self._storage
478
479
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500480def now():
481 """Exists so it can be mocked easily."""
482 return time.time()
483
484
maruel77f720b2015-09-15 12:35:22 -0700485def parse_time(value):
486 """Converts serialized time from the API to datetime.datetime."""
487 # When microseconds are 0, the '.123456' suffix is elided. This means the
488 # serialized format is not consistent, which confuses the hell out of python.
489 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
490 try:
491 return datetime.datetime.strptime(value, fmt)
492 except ValueError:
493 pass
494 raise ValueError('Failed to parse %s' % value)
495
496
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700497def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700498 base_url, shard_index, task_id, timeout, should_stop, output_collector,
499 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400500 """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)
maruel9531ce02016-04-13 06:11:23 -0700508 if include_perf:
509 result_url += '?include_performance_stats=true'
maruel77f720b2015-09-15 12:35:22 -0700510 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700511 started = now()
512 deadline = started + timeout if timeout else None
513 attempt = 0
514
515 while not should_stop.is_set():
516 attempt += 1
517
518 # Waiting for too long -> give up.
519 current_time = now()
520 if deadline and current_time >= deadline:
521 logging.error('retrieve_results(%s) timed out on attempt %d',
522 base_url, attempt)
523 return None
524
525 # Do not spin too fast. Spin faster at the beginning though.
526 # Start with 1 sec delay and for each 30 sec of waiting add another second
527 # of delay, until hitting 15 sec ceiling.
528 if attempt > 1:
529 max_delay = min(15, 1 + (current_time - started) / 30.0)
530 delay = min(max_delay, deadline - current_time) if deadline else max_delay
531 if delay > 0:
532 logging.debug('Waiting %.1f sec before retrying', delay)
533 should_stop.wait(delay)
534 if should_stop.is_set():
535 return None
536
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400537 # Disable internal retries in net.url_read_json, since we are doing retries
538 # ourselves.
539 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700540 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
541 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400542 result = net.url_read_json(result_url, retry_50x=False)
543 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400544 continue
maruel77f720b2015-09-15 12:35:22 -0700545
maruelbf53e042015-12-01 15:00:51 -0800546 if result.get('error'):
547 # An error occurred.
548 if result['error'].get('errors'):
549 for err in result['error']['errors']:
550 logging.warning(
551 'Error while reading task: %s; %s',
552 err.get('message'), err.get('debugInfo'))
553 elif result['error'].get('message'):
554 logging.warning(
555 'Error while reading task: %s', result['error']['message'])
556 continue
557
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400558 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700559 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400560 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700561 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700562 # Record the result, try to fetch attached output files (if any).
563 if output_collector:
564 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700565 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700566 if result.get('internal_failure'):
567 logging.error('Internal error!')
568 elif result['state'] == 'BOT_DIED':
569 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700570 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000571
572
maruel77f720b2015-09-15 12:35:22 -0700573def convert_to_old_format(result):
574 """Converts the task result data from Endpoints API format to old API format
575 for compatibility.
576
577 This goes into the file generated as --task-summary-json.
578 """
579 # Sets default.
580 result.setdefault('abandoned_ts', None)
581 result.setdefault('bot_id', None)
582 result.setdefault('bot_version', None)
583 result.setdefault('children_task_ids', [])
584 result.setdefault('completed_ts', None)
585 result.setdefault('cost_saved_usd', None)
586 result.setdefault('costs_usd', None)
587 result.setdefault('deduped_from', None)
588 result.setdefault('name', None)
589 result.setdefault('outputs_ref', None)
590 result.setdefault('properties_hash', None)
591 result.setdefault('server_versions', None)
592 result.setdefault('started_ts', None)
593 result.setdefault('tags', None)
594 result.setdefault('user', None)
595
596 # Convertion back to old API.
597 duration = result.pop('duration', None)
598 result['durations'] = [duration] if duration else []
599 exit_code = result.pop('exit_code', None)
600 result['exit_codes'] = [int(exit_code)] if exit_code else []
601 result['id'] = result.pop('task_id')
602 result['isolated_out'] = result.get('outputs_ref', None)
603 output = result.pop('output', None)
604 result['outputs'] = [output] if output else []
605 # properties_hash
606 # server_version
607 # Endpoints result 'state' as string. For compatibility with old code, convert
608 # to int.
609 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700610 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700611 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700612 if 'bot_dimensions' in result:
613 result['bot_dimensions'] = {
614 i['key']: i['value'] for i in result['bot_dimensions']
615 }
616 else:
617 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700618
619
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700620def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400621 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700622 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500623 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700625 Duplicate shards are ignored. Shards are yielded in order of completion.
626 Timed out shards are NOT yielded at all. Caller can compare number of yielded
627 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628
629 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500630 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 +0000631 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500632
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700633 output_collector is an optional instance of TaskOutputCollector that will be
634 used to fetch files produced by a task from isolate server to the local disk.
635
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500636 Yields:
637 (index, result). In particular, 'result' is defined as the
638 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700642 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700643 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700644
maruel@chromium.org0437a732013-08-27 16:05:52 +0000645 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
646 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700647 # Adds a task to the thread pool to call 'retrieve_results' and return
648 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400649 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000651 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400652 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700653 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654
655 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400656 for shard_index, task_id in enumerate(task_ids):
657 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700658
659 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400660 shards_remaining = range(len(task_ids))
661 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700662 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700663 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700664 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700665 shard_index, result = results_channel.pull(
666 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700667 except threading_utils.TaskChannel.Timeout:
668 if print_status_updates:
669 print(
670 'Waiting for results from the following shards: %s' %
671 ', '.join(map(str, shards_remaining)))
672 sys.stdout.flush()
673 continue
674 except Exception:
675 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700676
677 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500680 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700682
Vadim Shtayurab450c602014-05-12 19:23:25 -0700683 # Yield back results to the caller.
684 assert shard_index in shards_remaining
685 shards_remaining.remove(shard_index)
686 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700687
maruel@chromium.org0437a732013-08-27 16:05:52 +0000688 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700689 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 should_stop.set()
691
692
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400693def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000694 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700695 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400696 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700697 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
698 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400699 else:
700 pending = 'N/A'
701
maruel77f720b2015-09-15 12:35:22 -0700702 if metadata.get('duration') is not None:
703 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400704 else:
705 duration = 'N/A'
706
maruel77f720b2015-09-15 12:35:22 -0700707 if metadata.get('exit_code') is not None:
708 # Integers are encoded as string to not loose precision.
709 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400710 else:
711 exit_code = 'N/A'
712
713 bot_id = metadata.get('bot_id') or 'N/A'
714
maruel77f720b2015-09-15 12:35:22 -0700715 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400716 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400717 tag_footer = (
718 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
719 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400720
721 tag_len = max(len(tag_header), len(tag_footer))
722 dash_pad = '+-%s-+\n' % ('-' * tag_len)
723 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
724 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
725
726 header = dash_pad + tag_header + dash_pad
727 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700728 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400729 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000730
731
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700732def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700733 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700734 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700735 """Retrieves results of a Swarming task.
736
737 Returns:
738 process exit code that should be returned to the user.
739 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700740 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700741 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700742
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700743 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700744 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400745 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400747 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400748 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700749 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700750 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700751
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400752 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700753 shard_exit_code = metadata.get('exit_code')
754 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700755 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700756 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700757 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400758 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700759 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700760
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700761 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400762 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400763 if len(seen_shards) < len(task_ids):
764 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700765 else:
maruel77f720b2015-09-15 12:35:22 -0700766 print('%s: %s %s' % (
767 metadata.get('bot_id', 'N/A'),
768 metadata['task_id'],
769 shard_exit_code))
770 if metadata['output']:
771 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400772 if output:
773 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700774 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700775 summary = output_collector.finalize()
776 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700777 # TODO(maruel): Make this optional.
778 for i in summary['shards']:
779 if i:
780 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700781 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400783 if decorate and total_duration:
784 print('Total duration: %.1fs' % total_duration)
785
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400786 if len(seen_shards) != len(task_ids):
787 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700788 print >> sys.stderr, ('Results from some shards are missing: %s' %
789 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700790 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700791
maruela5490782015-09-30 10:56:59 -0700792 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000793
794
maruel77f720b2015-09-15 12:35:22 -0700795### API management.
796
797
798class APIError(Exception):
799 pass
800
801
802def endpoints_api_discovery_apis(host):
803 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
804 the APIs exposed by a host.
805
806 https://developers.google.com/discovery/v1/reference/apis/list
807 """
808 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
809 if data is None:
810 raise APIError('Failed to discover APIs on %s' % host)
811 out = {}
812 for api in data['items']:
813 if api['id'] == 'discovery:v1':
814 continue
815 # URL is of the following form:
816 # url = host + (
817 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
818 api_data = net.url_read_json(api['discoveryRestUrl'])
819 if api_data is None:
820 raise APIError('Failed to discover %s on %s' % (api['id'], host))
821 out[api['id']] = api_data
822 return out
823
824
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500825### Commands.
826
827
828def abort_task(_swarming, _manifest):
829 """Given a task manifest that was triggered, aborts its execution."""
830 # TODO(vadimsh): No supported by the server yet.
831
832
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400833def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400834 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500835 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500836 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500837 dest='dimensions', metavar='FOO bar',
838 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500839 parser.add_option_group(parser.filter_group)
840
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400841
Vadim Shtayurab450c602014-05-12 19:23:25 -0700842def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400843 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700844 parser.sharding_group.add_option(
845 '--shards', type='int', default=1,
846 help='Number of shards to trigger and collect.')
847 parser.add_option_group(parser.sharding_group)
848
849
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400850def add_trigger_options(parser):
851 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500852 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400853 add_filter_options(parser)
854
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400855 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500856 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500857 '-s', '--isolated',
858 help='Hash of the .isolated to grab from the isolate server')
859 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500860 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700861 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500862 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500863 '--priority', type='int', default=100,
864 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500865 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500866 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400867 help='Display name of the task. Defaults to '
868 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
869 'isolated file is provided, if a hash is provided, it defaults to '
870 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400871 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400872 '--tags', action='append', default=[],
873 help='Tags to assign to the task.')
874 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500875 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400876 help='User associated with the task. Defaults to authenticated user on '
877 'the server.')
878 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400879 '--idempotent', action='store_true', default=False,
880 help='When set, the server will actively try to find a previous task '
881 'with the same parameter and return this result instead if possible')
882 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400883 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400884 help='Seconds to allow the task to be pending for a bot to run before '
885 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400886 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400887 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400888 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400889 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400890 '--hard-timeout', type='int', default=60*60,
891 help='Seconds to allow the task to complete.')
892 parser.task_group.add_option(
893 '--io-timeout', type='int', default=20*60,
894 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500895 parser.task_group.add_option(
896 '--raw-cmd', action='store_true', default=False,
897 help='When set, the command after -- is used as-is without run_isolated. '
898 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500899 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000900
901
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500902def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500903 """Processes trigger options and uploads files to isolate server if necessary.
904 """
905 options.dimensions = dict(options.dimensions)
906 options.env = dict(options.env)
907
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500908 if not options.dimensions:
909 parser.error('Please at least specify one --dimension')
910 if options.raw_cmd:
911 if not args:
912 parser.error(
913 'Arguments with --raw-cmd should be passed after -- as command '
914 'delimiter.')
915 if options.isolate_server:
916 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
917
918 command = args
919 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500920 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500921 options.user,
922 '_'.join(
923 '%s=%s' % (k, v)
924 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700925 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500926 else:
nodir55be77b2016-05-03 09:39:57 -0700927 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500928 try:
maruel77f720b2015-09-15 12:35:22 -0700929 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500930 except ValueError as e:
931 parser.error(str(e))
932
nodir152cba62016-05-12 16:08:56 -0700933 # If inputs_ref.isolated is used, command is actually extra_args.
934 # Otherwise it's an actual command to run.
935 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -0700936 properties = TaskProperties(
nodir152cba62016-05-12 16:08:56 -0700937 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500938 dimensions=options.dimensions,
939 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700940 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -0700941 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -0700942 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500943 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700944 inputs_ref=inputs_ref,
945 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700946 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
947 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700948 return NewTaskRequest(
949 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500950 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700951 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500952 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700953 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500954 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700955 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000956
957
958def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500959 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -0800960 '-t', '--timeout', type='float',
961 help='Timeout to wait for result, set to 0 for no timeout; default to no '
962 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500963 parser.group_logging.add_option(
964 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700965 parser.group_logging.add_option(
966 '--print-status-updates', action='store_true',
967 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400968 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700969 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700970 '--task-summary-json',
971 metavar='FILE',
972 help='Dump a summary of task results to this file as json. It contains '
973 'only shards statuses as know to server directly. Any output files '
974 'emitted by the task can be collected by using --task-output-dir')
975 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700976 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700977 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700978 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700979 'directory contains per-shard directory with output files produced '
980 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -0700981 parser.task_output_group.add_option(
982 '--perf', action='store_true', default=False,
983 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700984 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000985
986
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400987@subcommand.usage('bots...')
988def CMDbot_delete(parser, args):
989 """Forcibly deletes bots from the Swarming server."""
990 parser.add_option(
991 '-f', '--force', action='store_true',
992 help='Do not prompt for confirmation')
993 options, args = parser.parse_args(args)
994 if not args:
995 parser.error('Please specific bots to delete')
996
997 bots = sorted(args)
998 if not options.force:
999 print('Delete the following bots?')
1000 for bot in bots:
1001 print(' %s' % bot)
1002 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1003 print('Goodbye.')
1004 return 1
1005
1006 result = 0
1007 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001008 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1009 if net.url_read_json(url, data={}, method='POST') is None:
1010 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001011 result = 1
1012 return result
1013
1014
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001015def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001016 """Returns information about the bots connected to the Swarming server."""
1017 add_filter_options(parser)
1018 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001019 '--dead-only', action='store_true',
1020 help='Only print dead bots, useful to reap them and reimage broken bots')
1021 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001022 '-k', '--keep-dead', action='store_true',
1023 help='Do not filter out dead bots')
1024 parser.filter_group.add_option(
1025 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001026 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001027 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001028
1029 if options.keep_dead and options.dead_only:
1030 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001031
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001032 bots = []
1033 cursor = None
1034 limit = 250
1035 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001036 base_url = (
1037 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001038 while True:
1039 url = base_url
1040 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001041 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001042 data = net.url_read_json(url)
1043 if data is None:
1044 print >> sys.stderr, 'Failed to access %s' % options.swarming
1045 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001046 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001047 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001048 if not cursor:
1049 break
1050
maruel77f720b2015-09-15 12:35:22 -07001051 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001052 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001053 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001054 continue
maruel77f720b2015-09-15 12:35:22 -07001055 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001056 continue
1057
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001058 # If the user requested to filter on dimensions, ensure the bot has all the
1059 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001060 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001061 for key, value in options.dimensions:
1062 if key not in dimensions:
1063 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001064 # A bot can have multiple value for a key, for example,
1065 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1066 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001067 if isinstance(dimensions[key], list):
1068 if value not in dimensions[key]:
1069 break
1070 else:
1071 if value != dimensions[key]:
1072 break
1073 else:
maruel77f720b2015-09-15 12:35:22 -07001074 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001075 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001076 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001077 if bot.get('task_id'):
1078 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001079 return 0
1080
1081
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001082@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001083def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001084 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001085
1086 The result can be in multiple part if the execution was sharded. It can
1087 potentially have retries.
1088 """
1089 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001090 parser.add_option(
1091 '-j', '--json',
1092 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001093 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001094 if not args and not options.json:
1095 parser.error('Must specify at least one task id or --json.')
1096 if args and options.json:
1097 parser.error('Only use one of task id or --json.')
1098
1099 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001100 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001101 try:
maruel1ceb3872015-10-14 06:10:44 -07001102 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001103 data = json.load(f)
1104 except (IOError, ValueError):
1105 parser.error('Failed to open %s' % options.json)
1106 try:
1107 tasks = sorted(
1108 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1109 args = [t['task_id'] for t in tasks]
1110 except (KeyError, TypeError):
1111 parser.error('Failed to process %s' % options.json)
1112 if options.timeout is None:
1113 options.timeout = (
1114 data['request']['properties']['execution_timeout_secs'] +
1115 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001116 else:
1117 valid = frozenset('0123456789abcdef')
1118 if any(not valid.issuperset(task_id) for task_id in args):
1119 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001120
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001121 try:
1122 return collect(
1123 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001124 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001125 options.timeout,
1126 options.decorate,
1127 options.print_status_updates,
1128 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001129 options.task_output_dir,
1130 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001131 except Failure:
1132 on_error.report(None)
1133 return 1
1134
1135
maruelbea00862015-09-18 09:55:36 -07001136@subcommand.usage('[filename]')
1137def CMDput_bootstrap(parser, args):
1138 """Uploads a new version of bootstrap.py."""
1139 options, args = parser.parse_args(args)
1140 if len(args) != 1:
1141 parser.error('Must specify file to upload')
1142 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001143 path = unicode(os.path.abspath(args[0]))
1144 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001145 content = f.read().decode('utf-8')
1146 data = net.url_read_json(url, data={'content': content})
1147 print data
1148 return 0
1149
1150
1151@subcommand.usage('[filename]')
1152def CMDput_bot_config(parser, args):
1153 """Uploads a new version of bot_config.py."""
1154 options, args = parser.parse_args(args)
1155 if len(args) != 1:
1156 parser.error('Must specify file to upload')
1157 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001158 path = unicode(os.path.abspath(args[0]))
1159 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001160 content = f.read().decode('utf-8')
1161 data = net.url_read_json(url, data={'content': content})
1162 print data
1163 return 0
1164
1165
maruel77f720b2015-09-15 12:35:22 -07001166@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001167def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001168 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1169 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001170
1171 Examples:
maruel77f720b2015-09-15 12:35:22 -07001172 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001173 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001174
maruel77f720b2015-09-15 12:35:22 -07001175 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001176 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1177
1178 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1179 quoting is important!:
1180 swarming.py query -S server-url.com --limit 10 \\
1181 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001182 """
1183 CHUNK_SIZE = 250
1184
1185 parser.add_option(
1186 '-L', '--limit', type='int', default=200,
1187 help='Limit to enforce on limitless items (like number of tasks); '
1188 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001189 parser.add_option(
1190 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001191 parser.add_option(
1192 '--progress', action='store_true',
1193 help='Prints a dot at each request to show progress')
1194 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001195 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001196 parser.error(
1197 'Must specify only method name and optionally query args properly '
1198 'escaped.')
1199 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001200 url = base_url
1201 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001202 # Check check, change if not working out.
1203 merge_char = '&' if '?' in url else '?'
1204 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001205 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001206 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001207 # TODO(maruel): Do basic diagnostic.
1208 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001209 return 1
1210
1211 # Some items support cursors. Try to get automatically if cursors are needed
1212 # by looking at the 'cursor' items.
1213 while (
1214 data.get('cursor') and
1215 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001216 merge_char = '&' if '?' in base_url else '?'
1217 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001218 if options.limit:
1219 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001220 if options.progress:
1221 sys.stdout.write('.')
1222 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001223 new = net.url_read_json(url)
1224 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001225 if options.progress:
1226 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001227 print >> sys.stderr, 'Failed to access %s' % options.swarming
1228 return 1
maruel81b37132015-10-21 06:42:13 -07001229 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001230 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001231
maruel77f720b2015-09-15 12:35:22 -07001232 if options.progress:
1233 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001234 if options.limit and len(data.get('items', [])) > options.limit:
1235 data['items'] = data['items'][:options.limit]
1236 data.pop('cursor', None)
1237
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001238 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001239 options.json = unicode(os.path.abspath(options.json))
1240 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001241 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001242 try:
maruel77f720b2015-09-15 12:35:22 -07001243 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001244 sys.stdout.write('\n')
1245 except IOError:
1246 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001247 return 0
1248
1249
maruel77f720b2015-09-15 12:35:22 -07001250def CMDquery_list(parser, args):
1251 """Returns list of all the Swarming APIs that can be used with command
1252 'query'.
1253 """
1254 parser.add_option(
1255 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1256 options, args = parser.parse_args(args)
1257 if args:
1258 parser.error('No argument allowed.')
1259
1260 try:
1261 apis = endpoints_api_discovery_apis(options.swarming)
1262 except APIError as e:
1263 parser.error(str(e))
1264 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001265 options.json = unicode(os.path.abspath(options.json))
1266 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001267 json.dump(apis, f)
1268 else:
1269 help_url = (
1270 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1271 options.swarming)
1272 for api_id, api in sorted(apis.iteritems()):
1273 print api_id
1274 print ' ' + api['description']
1275 for resource_name, resource in sorted(api['resources'].iteritems()):
1276 print ''
1277 for method_name, method in sorted(resource['methods'].iteritems()):
1278 # Only list the GET ones.
1279 if method['httpMethod'] != 'GET':
1280 continue
1281 print '- %s.%s: %s' % (
1282 resource_name, method_name, method['path'])
1283 print ' ' + method['description']
1284 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1285 return 0
1286
1287
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001288@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001289def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001290 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001291
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001292 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001293 """
1294 add_trigger_options(parser)
1295 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001296 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001297 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001298 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001299 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001300 tasks = trigger_task_shards(
1301 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001302 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001303 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001304 'Failed to trigger %s(%s): %s' %
1305 (options.task_name, args[0], e.args[0]))
1306 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001307 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001308 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001309 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001310 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001311 task_ids = [
1312 t['task_id']
1313 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1314 ]
maruel71c61c82016-02-22 06:52:05 -08001315 if options.timeout is None:
1316 options.timeout = (
1317 task_request.properties.execution_timeout_secs +
1318 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001319 try:
1320 return collect(
1321 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001322 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001323 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001324 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001325 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001326 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001327 options.task_output_dir,
1328 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001329 except Failure:
1330 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001331 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001332
1333
maruel18122c62015-10-23 06:31:23 -07001334@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001335def CMDreproduce(parser, args):
1336 """Runs a task locally that was triggered on the server.
1337
1338 This running locally the same commands that have been run on the bot. The data
1339 downloaded will be in a subdirectory named 'work' of the current working
1340 directory.
maruel18122c62015-10-23 06:31:23 -07001341
1342 You can pass further additional arguments to the target command by passing
1343 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001344 """
maruelc070e672016-02-22 17:32:57 -08001345 parser.add_option(
1346 '--output-dir', metavar='DIR', default='',
1347 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001348 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001349 extra_args = []
1350 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001351 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001352 if len(args) > 1:
1353 if args[1] == '--':
1354 if len(args) > 2:
1355 extra_args = args[2:]
1356 else:
1357 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001358
maruel77f720b2015-09-15 12:35:22 -07001359 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001360 request = net.url_read_json(url)
1361 if not request:
1362 print >> sys.stderr, 'Failed to retrieve request data for the task'
1363 return 1
1364
maruel12e30012015-10-09 11:55:35 -07001365 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001366 if fs.isdir(workdir):
1367 parser.error('Please delete the directory \'work\' first')
1368 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001369
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001370 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001371 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001372 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001373 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001374 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001375 for i in properties['env']:
1376 key = i['key'].encode('utf-8')
1377 if not i['value']:
1378 env.pop(key, None)
1379 else:
1380 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001381
nodir152cba62016-05-12 16:08:56 -07001382 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001383 # Create the tree.
1384 with isolateserver.get_storage(
1385 properties['inputs_ref']['isolatedserver'],
1386 properties['inputs_ref']['namespace']) as storage:
1387 bundle = isolateserver.fetch_isolated(
1388 properties['inputs_ref']['isolated'],
1389 storage,
1390 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -07001391 workdir)
maruel29ab2fd2015-10-16 11:44:01 -07001392 command = bundle.command
1393 if bundle.relative_cwd:
1394 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001395 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001396 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1397 new_command = run_isolated.process_command(command, options.output_dir)
1398 if not options.output_dir and new_command != command:
1399 parser.error('The task has outputs, you must use --output-dir')
1400 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001401 else:
1402 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001403 try:
maruel18122c62015-10-23 06:31:23 -07001404 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001405 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001406 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001407 print >> sys.stderr, str(e)
1408 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001409
1410
maruel0eb1d1b2015-10-02 14:48:21 -07001411@subcommand.usage('bot_id')
1412def CMDterminate(parser, args):
1413 """Tells a bot to gracefully shut itself down as soon as it can.
1414
1415 This is done by completing whatever current task there is then exiting the bot
1416 process.
1417 """
1418 parser.add_option(
1419 '--wait', action='store_true', help='Wait for the bot to terminate')
1420 options, args = parser.parse_args(args)
1421 if len(args) != 1:
1422 parser.error('Please provide the bot id')
1423 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1424 request = net.url_read_json(url, data={})
1425 if not request:
1426 print >> sys.stderr, 'Failed to ask for termination'
1427 return 1
1428 if options.wait:
1429 return collect(
maruel9531ce02016-04-13 06:11:23 -07001430 options.swarming, [request['task_id']], 0., False, False, None, None,
1431 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001432 return 0
1433
1434
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001435@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001436def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001437 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001438
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001439 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001440 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001441
1442 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001443
1444 Passes all extra arguments provided after '--' as additional command line
1445 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001446 """
1447 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001448 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001449 parser.add_option(
1450 '--dump-json',
1451 metavar='FILE',
1452 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001453 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001454 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001455 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001456 tasks = trigger_task_shards(
1457 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001458 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001459 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001460 tasks_sorted = sorted(
1461 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001462 if options.dump_json:
1463 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001464 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001465 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001466 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001467 }
maruel46b015f2015-10-13 18:40:35 -07001468 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001469 print('To collect results, use:')
1470 print(' swarming.py collect -S %s --json %s' %
1471 (options.swarming, options.dump_json))
1472 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001473 print('To collect results, use:')
1474 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001475 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1476 print('Or visit:')
1477 for t in tasks_sorted:
1478 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001479 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001480 except Failure:
1481 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001482 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001483
1484
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001485class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001486 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001487 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001488 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001489 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001490 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001491 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001492 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001493 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001494 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001495 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001496
1497 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001498 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001499 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001500 auth.process_auth_options(self, options)
1501 user = self._process_swarming(options)
1502 if hasattr(options, 'user') and not options.user:
1503 options.user = user
1504 return options, args
1505
1506 def _process_swarming(self, options):
1507 """Processes the --swarming option and aborts if not specified.
1508
1509 Returns the identity as determined by the server.
1510 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001511 if not options.swarming:
1512 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001513 try:
1514 options.swarming = net.fix_url(options.swarming)
1515 except ValueError as e:
1516 self.error('--swarming %s' % e)
1517 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001518 try:
1519 user = auth.ensure_logged_in(options.swarming)
1520 except ValueError as e:
1521 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001522 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001523
1524
1525def main(args):
1526 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001527 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001528
1529
1530if __name__ == '__main__':
1531 fix_encoding.fix_encoding()
1532 tools.disable_buffering()
1533 colorama.init()
1534 sys.exit(main(sys.argv[1:]))