blob: 1169e18a2c46c06d647db6ec8593180458b51464 [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.
3# Use of this source code is governed by the Apache v2.0 license that can be
4# 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(
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
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:
927 isolateserver.process_isolate_server_options(parser, options, False)
928 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
maruel77f720b2015-09-15 12:35:22 -0700933 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
934 # actual command to run.
935 properties = TaskProperties(
936 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500937 dimensions=options.dimensions,
938 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700939 execution_timeout_secs=options.hard_timeout,
940 extra_args=command if inputs_ref else None,
941 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500942 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700943 inputs_ref=inputs_ref,
944 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700945 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
946 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700947 return NewTaskRequest(
948 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500949 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700950 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500951 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700952 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500953 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700954 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000955
956
957def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500958 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -0800959 '-t', '--timeout', type='float',
960 help='Timeout to wait for result, set to 0 for no timeout; default to no '
961 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500962 parser.group_logging.add_option(
963 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700964 parser.group_logging.add_option(
965 '--print-status-updates', action='store_true',
966 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400967 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700968 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700969 '--task-summary-json',
970 metavar='FILE',
971 help='Dump a summary of task results to this file as json. It contains '
972 'only shards statuses as know to server directly. Any output files '
973 'emitted by the task can be collected by using --task-output-dir')
974 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700975 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700976 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700977 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700978 'directory contains per-shard directory with output files produced '
979 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -0700980 parser.task_output_group.add_option(
981 '--perf', action='store_true', default=False,
982 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700983 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000984
985
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400986@subcommand.usage('bots...')
987def CMDbot_delete(parser, args):
988 """Forcibly deletes bots from the Swarming server."""
989 parser.add_option(
990 '-f', '--force', action='store_true',
991 help='Do not prompt for confirmation')
992 options, args = parser.parse_args(args)
993 if not args:
994 parser.error('Please specific bots to delete')
995
996 bots = sorted(args)
997 if not options.force:
998 print('Delete the following bots?')
999 for bot in bots:
1000 print(' %s' % bot)
1001 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1002 print('Goodbye.')
1003 return 1
1004
1005 result = 0
1006 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001007 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1008 if net.url_read_json(url, data={}, method='POST') is None:
1009 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001010 result = 1
1011 return result
1012
1013
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001014def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001015 """Returns information about the bots connected to the Swarming server."""
1016 add_filter_options(parser)
1017 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001018 '--dead-only', action='store_true',
1019 help='Only print dead bots, useful to reap them and reimage broken bots')
1020 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001021 '-k', '--keep-dead', action='store_true',
1022 help='Do not filter out dead bots')
1023 parser.filter_group.add_option(
1024 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001025 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001026 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001027
1028 if options.keep_dead and options.dead_only:
1029 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001030
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001031 bots = []
1032 cursor = None
1033 limit = 250
1034 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001035 base_url = (
1036 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001037 while True:
1038 url = base_url
1039 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001040 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001041 data = net.url_read_json(url)
1042 if data is None:
1043 print >> sys.stderr, 'Failed to access %s' % options.swarming
1044 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001045 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001046 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001047 if not cursor:
1048 break
1049
maruel77f720b2015-09-15 12:35:22 -07001050 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001051 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001052 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001053 continue
maruel77f720b2015-09-15 12:35:22 -07001054 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001055 continue
1056
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001057 # If the user requested to filter on dimensions, ensure the bot has all the
1058 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001059 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001060 for key, value in options.dimensions:
1061 if key not in dimensions:
1062 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001063 # A bot can have multiple value for a key, for example,
1064 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1065 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001066 if isinstance(dimensions[key], list):
1067 if value not in dimensions[key]:
1068 break
1069 else:
1070 if value != dimensions[key]:
1071 break
1072 else:
maruel77f720b2015-09-15 12:35:22 -07001073 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001074 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001075 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001076 if bot.get('task_id'):
1077 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001078 return 0
1079
1080
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001081@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001082def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001083 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001084
1085 The result can be in multiple part if the execution was sharded. It can
1086 potentially have retries.
1087 """
1088 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001089 parser.add_option(
1090 '-j', '--json',
1091 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001092 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001093 if not args and not options.json:
1094 parser.error('Must specify at least one task id or --json.')
1095 if args and options.json:
1096 parser.error('Only use one of task id or --json.')
1097
1098 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001099 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001100 try:
maruel1ceb3872015-10-14 06:10:44 -07001101 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001102 data = json.load(f)
1103 except (IOError, ValueError):
1104 parser.error('Failed to open %s' % options.json)
1105 try:
1106 tasks = sorted(
1107 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1108 args = [t['task_id'] for t in tasks]
1109 except (KeyError, TypeError):
1110 parser.error('Failed to process %s' % options.json)
1111 if options.timeout is None:
1112 options.timeout = (
1113 data['request']['properties']['execution_timeout_secs'] +
1114 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001115 else:
1116 valid = frozenset('0123456789abcdef')
1117 if any(not valid.issuperset(task_id) for task_id in args):
1118 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001119
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001120 try:
1121 return collect(
1122 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001123 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001124 options.timeout,
1125 options.decorate,
1126 options.print_status_updates,
1127 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001128 options.task_output_dir,
1129 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001130 except Failure:
1131 on_error.report(None)
1132 return 1
1133
1134
maruelbea00862015-09-18 09:55:36 -07001135@subcommand.usage('[filename]')
1136def CMDput_bootstrap(parser, args):
1137 """Uploads a new version of bootstrap.py."""
1138 options, args = parser.parse_args(args)
1139 if len(args) != 1:
1140 parser.error('Must specify file to upload')
1141 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001142 path = unicode(os.path.abspath(args[0]))
1143 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001144 content = f.read().decode('utf-8')
1145 data = net.url_read_json(url, data={'content': content})
1146 print data
1147 return 0
1148
1149
1150@subcommand.usage('[filename]')
1151def CMDput_bot_config(parser, args):
1152 """Uploads a new version of bot_config.py."""
1153 options, args = parser.parse_args(args)
1154 if len(args) != 1:
1155 parser.error('Must specify file to upload')
1156 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001157 path = unicode(os.path.abspath(args[0]))
1158 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001159 content = f.read().decode('utf-8')
1160 data = net.url_read_json(url, data={'content': content})
1161 print data
1162 return 0
1163
1164
maruel77f720b2015-09-15 12:35:22 -07001165@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001166def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001167 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1168 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001169
1170 Examples:
maruel77f720b2015-09-15 12:35:22 -07001171 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001172 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001173
maruel77f720b2015-09-15 12:35:22 -07001174 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001175 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1176
1177 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1178 quoting is important!:
1179 swarming.py query -S server-url.com --limit 10 \\
1180 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001181 """
1182 CHUNK_SIZE = 250
1183
1184 parser.add_option(
1185 '-L', '--limit', type='int', default=200,
1186 help='Limit to enforce on limitless items (like number of tasks); '
1187 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001188 parser.add_option(
1189 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001190 parser.add_option(
1191 '--progress', action='store_true',
1192 help='Prints a dot at each request to show progress')
1193 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001194 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001195 parser.error(
1196 'Must specify only method name and optionally query args properly '
1197 'escaped.')
1198 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001199 url = base_url
1200 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001201 # Check check, change if not working out.
1202 merge_char = '&' if '?' in url else '?'
1203 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001204 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001205 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001206 # TODO(maruel): Do basic diagnostic.
1207 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001208 return 1
1209
1210 # Some items support cursors. Try to get automatically if cursors are needed
1211 # by looking at the 'cursor' items.
1212 while (
1213 data.get('cursor') and
1214 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001215 merge_char = '&' if '?' in base_url else '?'
1216 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001217 if options.limit:
1218 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001219 if options.progress:
1220 sys.stdout.write('.')
1221 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001222 new = net.url_read_json(url)
1223 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001224 if options.progress:
1225 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001226 print >> sys.stderr, 'Failed to access %s' % options.swarming
1227 return 1
maruel81b37132015-10-21 06:42:13 -07001228 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001229 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001230
maruel77f720b2015-09-15 12:35:22 -07001231 if options.progress:
1232 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001233 if options.limit and len(data.get('items', [])) > options.limit:
1234 data['items'] = data['items'][:options.limit]
1235 data.pop('cursor', None)
1236
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001237 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001238 options.json = unicode(os.path.abspath(options.json))
1239 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001240 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001241 try:
maruel77f720b2015-09-15 12:35:22 -07001242 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001243 sys.stdout.write('\n')
1244 except IOError:
1245 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001246 return 0
1247
1248
maruel77f720b2015-09-15 12:35:22 -07001249def CMDquery_list(parser, args):
1250 """Returns list of all the Swarming APIs that can be used with command
1251 'query'.
1252 """
1253 parser.add_option(
1254 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1255 options, args = parser.parse_args(args)
1256 if args:
1257 parser.error('No argument allowed.')
1258
1259 try:
1260 apis = endpoints_api_discovery_apis(options.swarming)
1261 except APIError as e:
1262 parser.error(str(e))
1263 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001264 options.json = unicode(os.path.abspath(options.json))
1265 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001266 json.dump(apis, f)
1267 else:
1268 help_url = (
1269 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1270 options.swarming)
1271 for api_id, api in sorted(apis.iteritems()):
1272 print api_id
1273 print ' ' + api['description']
1274 for resource_name, resource in sorted(api['resources'].iteritems()):
1275 print ''
1276 for method_name, method in sorted(resource['methods'].iteritems()):
1277 # Only list the GET ones.
1278 if method['httpMethod'] != 'GET':
1279 continue
1280 print '- %s.%s: %s' % (
1281 resource_name, method_name, method['path'])
1282 print ' ' + method['description']
1283 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1284 return 0
1285
1286
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001287@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001288def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001289 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001290
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001291 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001292 """
1293 add_trigger_options(parser)
1294 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001295 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001296 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001297 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001298 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001299 tasks = trigger_task_shards(
1300 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001301 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001302 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001303 'Failed to trigger %s(%s): %s' %
1304 (options.task_name, args[0], e.args[0]))
1305 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001306 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001307 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001308 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001309 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001310 task_ids = [
1311 t['task_id']
1312 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1313 ]
maruel71c61c82016-02-22 06:52:05 -08001314 if options.timeout is None:
1315 options.timeout = (
1316 task_request.properties.execution_timeout_secs +
1317 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001318 try:
1319 return collect(
1320 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001321 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001322 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001323 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001324 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001325 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001326 options.task_output_dir,
1327 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001328 except Failure:
1329 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001330 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001331
1332
maruel18122c62015-10-23 06:31:23 -07001333@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001334def CMDreproduce(parser, args):
1335 """Runs a task locally that was triggered on the server.
1336
1337 This running locally the same commands that have been run on the bot. The data
1338 downloaded will be in a subdirectory named 'work' of the current working
1339 directory.
maruel18122c62015-10-23 06:31:23 -07001340
1341 You can pass further additional arguments to the target command by passing
1342 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001343 """
maruelc070e672016-02-22 17:32:57 -08001344 parser.add_option(
1345 '--output-dir', metavar='DIR', default='',
1346 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001347 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001348 extra_args = []
1349 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001350 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001351 if len(args) > 1:
1352 if args[1] == '--':
1353 if len(args) > 2:
1354 extra_args = args[2:]
1355 else:
1356 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001357
maruel77f720b2015-09-15 12:35:22 -07001358 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001359 request = net.url_read_json(url)
1360 if not request:
1361 print >> sys.stderr, 'Failed to retrieve request data for the task'
1362 return 1
1363
maruel12e30012015-10-09 11:55:35 -07001364 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001365 if fs.isdir(workdir):
1366 parser.error('Please delete the directory \'work\' first')
1367 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001368
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001369 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001370 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001371 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001372 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001373 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001374 for i in properties['env']:
1375 key = i['key'].encode('utf-8')
1376 if not i['value']:
1377 env.pop(key, None)
1378 else:
1379 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001380
maruel29ab2fd2015-10-16 11:44:01 -07001381 if properties.get('inputs_ref'):
1382 # Create the tree.
1383 with isolateserver.get_storage(
1384 properties['inputs_ref']['isolatedserver'],
1385 properties['inputs_ref']['namespace']) as storage:
1386 bundle = isolateserver.fetch_isolated(
1387 properties['inputs_ref']['isolated'],
1388 storage,
1389 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -07001390 workdir)
maruel29ab2fd2015-10-16 11:44:01 -07001391 command = bundle.command
1392 if bundle.relative_cwd:
1393 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001394 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001395 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1396 new_command = run_isolated.process_command(command, options.output_dir)
1397 if not options.output_dir and new_command != command:
1398 parser.error('The task has outputs, you must use --output-dir')
1399 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001400 else:
1401 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001402 try:
maruel18122c62015-10-23 06:31:23 -07001403 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001404 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001405 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001406 print >> sys.stderr, str(e)
1407 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001408
1409
maruel0eb1d1b2015-10-02 14:48:21 -07001410@subcommand.usage('bot_id')
1411def CMDterminate(parser, args):
1412 """Tells a bot to gracefully shut itself down as soon as it can.
1413
1414 This is done by completing whatever current task there is then exiting the bot
1415 process.
1416 """
1417 parser.add_option(
1418 '--wait', action='store_true', help='Wait for the bot to terminate')
1419 options, args = parser.parse_args(args)
1420 if len(args) != 1:
1421 parser.error('Please provide the bot id')
1422 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1423 request = net.url_read_json(url, data={})
1424 if not request:
1425 print >> sys.stderr, 'Failed to ask for termination'
1426 return 1
1427 if options.wait:
1428 return collect(
maruel9531ce02016-04-13 06:11:23 -07001429 options.swarming, [request['task_id']], 0., False, False, None, None,
1430 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001431 return 0
1432
1433
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001434@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001435def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001436 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001437
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001438 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001439 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001440
1441 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001442
1443 Passes all extra arguments provided after '--' as additional command line
1444 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001445 """
1446 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001447 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001448 parser.add_option(
1449 '--dump-json',
1450 metavar='FILE',
1451 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001452 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001453 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001454 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001455 tasks = trigger_task_shards(
1456 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001457 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001458 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001459 tasks_sorted = sorted(
1460 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001461 if options.dump_json:
1462 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001463 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001464 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001465 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001466 }
maruel46b015f2015-10-13 18:40:35 -07001467 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001468 print('To collect results, use:')
1469 print(' swarming.py collect -S %s --json %s' %
1470 (options.swarming, options.dump_json))
1471 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001472 print('To collect results, use:')
1473 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001474 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1475 print('Or visit:')
1476 for t in tasks_sorted:
1477 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001478 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001479 except Failure:
1480 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001481 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001482
1483
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001484class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001485 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001486 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001487 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001488 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001489 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001490 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001491 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001492 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001493 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001494 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001495
1496 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001497 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001498 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001499 auth.process_auth_options(self, options)
1500 user = self._process_swarming(options)
1501 if hasattr(options, 'user') and not options.user:
1502 options.user = user
1503 return options, args
1504
1505 def _process_swarming(self, options):
1506 """Processes the --swarming option and aborts if not specified.
1507
1508 Returns the identity as determined by the server.
1509 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001510 if not options.swarming:
1511 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001512 try:
1513 options.swarming = net.fix_url(options.swarming)
1514 except ValueError as e:
1515 self.error('--swarming %s' % e)
1516 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001517 try:
1518 user = auth.ensure_logged_in(options.swarming)
1519 except ValueError as e:
1520 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001521 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001522
1523
1524def main(args):
1525 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001526 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001527
1528
1529if __name__ == '__main__':
1530 fix_encoding.fix_encoding()
1531 tools.disable_buffering()
1532 colorama.init()
1533 sys.exit(main(sys.argv[1:]))