blob: 9fa8bbdfcf66d7457d8c0b4a5f1fbad6a3eab79b [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2013 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005
6"""Client tool to trigger tasks or retrieve results from a Swarming server."""
7
maruel77f720b2015-09-15 12:35:22 -07008__version__ = '0.8.2'
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
Vadim Shtayurab19319e2014-04-27 08:50:06 -070018import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000019import time
20import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000021
22from third_party import colorama
23from third_party.depot_tools import fix_encoding
24from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000025
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050026from utils import file_path
maruel12e30012015-10-09 11:55:35 -070027from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040028from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040029from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000030from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040031from utils import on_error
maruel@chromium.org0437a732013-08-27 16:05:52 +000032from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000033from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000034
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080035import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040036import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000037import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000038
39
40ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050041
42
43class Failure(Exception):
44 """Generic failure."""
45 pass
46
47
48### Isolated file handling.
49
50
maruel77f720b2015-09-15 12:35:22 -070051def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050052 """Archives a .isolated file if needed.
53
54 Returns the file hash to trigger and a bool specifying if it was a file (True)
55 or a hash (False).
56 """
57 if arg.endswith('.isolated'):
maruel77f720b2015-09-15 12:35:22 -070058 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050059 if not file_hash:
60 on_error.report('Archival failure %s' % arg)
61 return None, True
62 return file_hash, True
63 elif isolated_format.is_valid_hash(arg, algo):
64 return arg, False
65 else:
66 on_error.report('Invalid hash %s' % arg)
67 return None, False
68
69
70def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050071 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050072
73 Returns:
maruel77f720b2015-09-15 12:35:22 -070074 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050075 """
76 isolated_cmd_args = []
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050077 if not options.isolated:
78 if '--' in args:
79 index = args.index('--')
80 isolated_cmd_args = args[index+1:]
81 args = args[:index]
82 else:
83 # optparse eats '--' sometimes.
84 isolated_cmd_args = args[1:]
85 args = args[:1]
86 if len(args) != 1:
87 raise ValueError(
88 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
89 'process.')
90 # Old code. To be removed eventually.
91 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070092 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050093 if not options.isolated:
94 raise ValueError('Invalid argument %s' % args[0])
95 elif args:
96 is_file = False
97 if '--' in args:
98 index = args.index('--')
99 isolated_cmd_args = args[index+1:]
100 if index != 0:
101 raise ValueError('Unexpected arguments.')
102 else:
103 # optparse eats '--' sometimes.
104 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500105
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500106 # If a file name was passed, use its base name of the isolated hash.
107 # Otherwise, use user name as an approximation of a task name.
108 if not options.task_name:
109 if is_file:
110 key = os.path.splitext(os.path.basename(args[0]))[0]
111 else:
112 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500113 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500114 key,
115 '_'.join(
116 '%s=%s' % (k, v)
117 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500118 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500119
maruel77f720b2015-09-15 12:35:22 -0700120 inputs_ref = FilesRef(
121 isolated=options.isolated,
122 isolatedserver=options.isolate_server,
123 namespace=options.namespace)
124 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500125
126
127### Triggering.
128
129
maruel77f720b2015-09-15 12:35:22 -0700130# See ../appengine/swarming/swarming_rpcs.py.
131FilesRef = collections.namedtuple(
132 'FilesRef',
133 [
134 'isolated',
135 'isolatedserver',
136 'namespace',
137 ])
138
139
140# See ../appengine/swarming/swarming_rpcs.py.
141TaskProperties = collections.namedtuple(
142 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500143 [
144 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500145 'dimensions',
146 'env',
maruel77f720b2015-09-15 12:35:22 -0700147 'execution_timeout_secs',
148 'extra_args',
149 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500150 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700151 'inputs_ref',
152 'io_timeout_secs',
153 ])
154
155
156# See ../appengine/swarming/swarming_rpcs.py.
157NewTaskRequest = collections.namedtuple(
158 'NewTaskRequest',
159 [
160 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500161 'name',
maruel77f720b2015-09-15 12:35:22 -0700162 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500163 'priority',
maruel77f720b2015-09-15 12:35:22 -0700164 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500165 'tags',
166 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500167 ])
168
169
maruel77f720b2015-09-15 12:35:22 -0700170def namedtuple_to_dict(value):
171 """Recursively converts a namedtuple to a dict."""
172 out = dict(value._asdict())
173 for k, v in out.iteritems():
174 if hasattr(v, '_asdict'):
175 out[k] = namedtuple_to_dict(v)
176 return out
177
178
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500179def task_request_to_raw_request(task_request):
180 """Returns the json dict expected by the Swarming server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700181
182 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500183 """
maruel77f720b2015-09-15 12:35:22 -0700184 out = namedtuple_to_dict(task_request)
185 # Maps are not supported until protobuf v3.
186 out['properties']['dimensions'] = [
187 {'key': k, 'value': v}
188 for k, v in out['properties']['dimensions'].iteritems()
189 ]
190 out['properties']['dimensions'].sort(key=lambda x: x['key'])
191 out['properties']['env'] = [
192 {'key': k, 'value': v}
193 for k, v in out['properties']['env'].iteritems()
194 ]
195 out['properties']['env'].sort(key=lambda x: x['key'])
196 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197
198
maruel77f720b2015-09-15 12:35:22 -0700199def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500200 """Triggers a request on the Swarming server and returns the json data.
201
202 It's the low-level function.
203
204 Returns:
205 {
206 'request': {
207 'created_ts': u'2010-01-02 03:04:05',
208 'name': ..
209 },
210 'task_id': '12300',
211 }
212 """
213 logging.info('Triggering: %s', raw_request['name'])
214
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500215 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700216 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500217 if not result:
218 on_error.report('Failed to trigger task %s' % raw_request['name'])
219 return None
220 return result
221
222
223def setup_googletest(env, shards, index):
224 """Sets googletest specific environment variables."""
225 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700226 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
227 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
228 env = env[:]
229 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
230 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500231 return env
232
233
234def trigger_task_shards(swarming, task_request, shards):
235 """Triggers one or many subtasks of a sharded task.
236
237 Returns:
238 Dict with task details, returned to caller as part of --dump-json output.
239 None in case of failure.
240 """
241 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700242 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500243 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700244 req['properties']['env'] = setup_googletest(
245 req['properties']['env'], shards, index)
246 req['name'] += ':%s:%s' % (index, shards)
247 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248
249 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250 tasks = {}
251 priority_warning = False
252 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700253 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500254 if not task:
255 break
256 logging.info('Request result: %s', task)
257 if (not priority_warning and
258 task['request']['priority'] != task_request.priority):
259 priority_warning = True
260 print >> sys.stderr, (
261 'Priority was reset to %s' % task['request']['priority'])
262 tasks[request['name']] = {
263 'shard_index': index,
264 'task_id': task['task_id'],
265 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
266 }
267
268 # Some shards weren't triggered. Abort everything.
269 if len(tasks) != len(requests):
270 if tasks:
271 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
272 len(tasks), len(requests))
273 for task_dict in tasks.itervalues():
274 abort_task(swarming, task_dict['task_id'])
275 return None
276
277 return tasks
278
279
280### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000281
282
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700283# How often to print status updates to stdout in 'collect'.
284STATUS_UPDATE_INTERVAL = 15 * 60.
285
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400286
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400287class State(object):
288 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000289
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400290 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
291 values are part of the API so if they change, the API changed.
292
293 It's in fact an enum. Values should be in decreasing order of importance.
294 """
295 RUNNING = 0x10
296 PENDING = 0x20
297 EXPIRED = 0x30
298 TIMED_OUT = 0x40
299 BOT_DIED = 0x50
300 CANCELED = 0x60
301 COMPLETED = 0x70
302
maruel77f720b2015-09-15 12:35:22 -0700303 STATES = (
304 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
305 'COMPLETED')
306 STATES_RUNNING = ('RUNNING', 'PENDING')
307 STATES_NOT_RUNNING = (
308 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
309 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
310 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400311
312 _NAMES = {
313 RUNNING: 'Running',
314 PENDING: 'Pending',
315 EXPIRED: 'Expired',
316 TIMED_OUT: 'Execution timed out',
317 BOT_DIED: 'Bot died',
318 CANCELED: 'User canceled',
319 COMPLETED: 'Completed',
320 }
321
maruel77f720b2015-09-15 12:35:22 -0700322 _ENUMS = {
323 'RUNNING': RUNNING,
324 'PENDING': PENDING,
325 'EXPIRED': EXPIRED,
326 'TIMED_OUT': TIMED_OUT,
327 'BOT_DIED': BOT_DIED,
328 'CANCELED': CANCELED,
329 'COMPLETED': COMPLETED,
330 }
331
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400332 @classmethod
333 def to_string(cls, state):
334 """Returns a user-readable string representing a State."""
335 if state not in cls._NAMES:
336 raise ValueError('Invalid state %s' % state)
337 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000338
maruel77f720b2015-09-15 12:35:22 -0700339 @classmethod
340 def from_enum(cls, state):
341 """Returns int value based on the string."""
342 if state not in cls._ENUMS:
343 raise ValueError('Invalid state %s' % state)
344 return cls._ENUMS[state]
345
maruel@chromium.org0437a732013-08-27 16:05:52 +0000346
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700347class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700348 """Assembles task execution summary (for --task-summary-json output).
349
350 Optionally fetches task outputs from isolate server to local disk (used when
351 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700352
353 This object is shared among multiple threads running 'retrieve_results'
354 function, in particular they call 'process_shard_result' method in parallel.
355 """
356
maruel0eb1d1b2015-10-02 14:48:21 -0700357 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700358 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
359
360 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700361 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362 shard_count: expected number of task shards.
363 """
maruel12e30012015-10-09 11:55:35 -0700364 self.task_output_dir = (
365 unicode(os.path.abspath(task_output_dir))
366 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700367 self.shard_count = shard_count
368
369 self._lock = threading.Lock()
370 self._per_shard_results = {}
371 self._storage = None
372
maruel12e30012015-10-09 11:55:35 -0700373 if self.task_output_dir and not fs.isdir(self.task_output_dir):
374 fs.makedirs(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700375
Vadim Shtayurab450c602014-05-12 19:23:25 -0700376 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377 """Stores results of a single task shard, fetches output files if necessary.
378
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400379 Modifies |result| in place.
380
maruel77f720b2015-09-15 12:35:22 -0700381 shard_index is 0-based.
382
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700383 Called concurrently from multiple threads.
384 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700386 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 if shard_index < 0 or shard_index >= self.shard_count:
388 logging.warning(
389 'Shard index %d is outside of expected range: [0; %d]',
390 shard_index, self.shard_count - 1)
391 return
392
maruel77f720b2015-09-15 12:35:22 -0700393 if result.get('outputs_ref'):
394 ref = result['outputs_ref']
395 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
396 ref['isolatedserver'],
397 urllib.urlencode(
398 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400399
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400 # Store result dict of that shard, ignore results we've already seen.
401 with self._lock:
402 if shard_index in self._per_shard_results:
403 logging.warning('Ignoring duplicate shard index %d', shard_index)
404 return
405 self._per_shard_results[shard_index] = result
406
407 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700408 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400409 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700410 result['outputs_ref']['isolatedserver'],
411 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400412 if storage:
413 # Output files are supposed to be small and they are not reused across
414 # tasks. So use MemoryCache for them instead of on-disk cache. Make
415 # files writable, so that calling script can delete them.
416 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700417 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400418 storage,
419 isolateserver.MemoryCache(file_mode_mask=0700),
420 os.path.join(self.task_output_dir, str(shard_index)),
421 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700422
423 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700424 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700425 with self._lock:
426 # Write an array of shard results with None for missing shards.
427 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700428 'shards': [
429 self._per_shard_results.get(i) for i in xrange(self.shard_count)
430 ],
431 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700432 # Write summary.json to task_output_dir as well.
433 if self.task_output_dir:
434 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700435 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700436 summary,
437 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438 if self._storage:
439 self._storage.close()
440 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700441 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442
443 def _get_storage(self, isolate_server, namespace):
444 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700445 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700446 with self._lock:
447 if not self._storage:
448 self._storage = isolateserver.get_storage(isolate_server, namespace)
449 else:
450 # Shards must all use exact same isolate server and namespace.
451 if self._storage.location != isolate_server:
452 logging.error(
453 'Task shards are using multiple isolate servers: %s and %s',
454 self._storage.location, isolate_server)
455 return None
456 if self._storage.namespace != namespace:
457 logging.error(
458 'Task shards are using multiple namespaces: %s and %s',
459 self._storage.namespace, namespace)
460 return None
461 return self._storage
462
463
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500464def now():
465 """Exists so it can be mocked easily."""
466 return time.time()
467
468
maruel77f720b2015-09-15 12:35:22 -0700469def parse_time(value):
470 """Converts serialized time from the API to datetime.datetime."""
471 # When microseconds are 0, the '.123456' suffix is elided. This means the
472 # serialized format is not consistent, which confuses the hell out of python.
473 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
474 try:
475 return datetime.datetime.strptime(value, fmt)
476 except ValueError:
477 pass
478 raise ValueError('Failed to parse %s' % value)
479
480
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700481def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400482 base_url, shard_index, task_id, timeout, should_stop, output_collector):
483 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700484
Vadim Shtayurab450c602014-05-12 19:23:25 -0700485 Returns:
486 <result dict> on success.
487 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700488 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000489 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700490 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
491 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700492 started = now()
493 deadline = started + timeout if timeout else None
494 attempt = 0
495
496 while not should_stop.is_set():
497 attempt += 1
498
499 # Waiting for too long -> give up.
500 current_time = now()
501 if deadline and current_time >= deadline:
502 logging.error('retrieve_results(%s) timed out on attempt %d',
503 base_url, attempt)
504 return None
505
506 # Do not spin too fast. Spin faster at the beginning though.
507 # Start with 1 sec delay and for each 30 sec of waiting add another second
508 # of delay, until hitting 15 sec ceiling.
509 if attempt > 1:
510 max_delay = min(15, 1 + (current_time - started) / 30.0)
511 delay = min(max_delay, deadline - current_time) if deadline else max_delay
512 if delay > 0:
513 logging.debug('Waiting %.1f sec before retrying', delay)
514 should_stop.wait(delay)
515 if should_stop.is_set():
516 return None
517
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400518 # Disable internal retries in net.url_read_json, since we are doing retries
519 # ourselves.
520 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700521 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
522 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400523 result = net.url_read_json(result_url, retry_50x=False)
524 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400525 continue
maruel77f720b2015-09-15 12:35:22 -0700526
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400527 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700528 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400529 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700530 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700531 # Record the result, try to fetch attached output files (if any).
532 if output_collector:
533 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700534 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700535 if result.get('internal_failure'):
536 logging.error('Internal error!')
537 elif result['state'] == 'BOT_DIED':
538 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700539 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000540
541
maruel77f720b2015-09-15 12:35:22 -0700542def convert_to_old_format(result):
543 """Converts the task result data from Endpoints API format to old API format
544 for compatibility.
545
546 This goes into the file generated as --task-summary-json.
547 """
548 # Sets default.
549 result.setdefault('abandoned_ts', None)
550 result.setdefault('bot_id', None)
551 result.setdefault('bot_version', None)
552 result.setdefault('children_task_ids', [])
553 result.setdefault('completed_ts', None)
554 result.setdefault('cost_saved_usd', None)
555 result.setdefault('costs_usd', None)
556 result.setdefault('deduped_from', None)
557 result.setdefault('name', None)
558 result.setdefault('outputs_ref', None)
559 result.setdefault('properties_hash', None)
560 result.setdefault('server_versions', None)
561 result.setdefault('started_ts', None)
562 result.setdefault('tags', None)
563 result.setdefault('user', None)
564
565 # Convertion back to old API.
566 duration = result.pop('duration', None)
567 result['durations'] = [duration] if duration else []
568 exit_code = result.pop('exit_code', None)
569 result['exit_codes'] = [int(exit_code)] if exit_code else []
570 result['id'] = result.pop('task_id')
571 result['isolated_out'] = result.get('outputs_ref', None)
572 output = result.pop('output', None)
573 result['outputs'] = [output] if output else []
574 # properties_hash
575 # server_version
576 # Endpoints result 'state' as string. For compatibility with old code, convert
577 # to int.
578 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700579 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700580 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700581 if 'bot_dimensions' in result:
582 result['bot_dimensions'] = {
583 i['key']: i['value'] for i in result['bot_dimensions']
584 }
585 else:
586 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700587
588
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700589def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400590 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
591 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500592 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000593
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700594 Duplicate shards are ignored. Shards are yielded in order of completion.
595 Timed out shards are NOT yielded at all. Caller can compare number of yielded
596 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000597
598 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500599 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 +0000600 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500601
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700602 output_collector is an optional instance of TaskOutputCollector that will be
603 used to fetch files produced by a task from isolate server to the local disk.
604
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500605 Yields:
606 (index, result). In particular, 'result' is defined as the
607 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000608 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000609 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400610 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700611 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700612 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700613
maruel@chromium.org0437a732013-08-27 16:05:52 +0000614 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
615 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700616 # Adds a task to the thread pool to call 'retrieve_results' and return
617 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400618 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700619 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000620 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400621 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
622 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700623
624 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400625 for shard_index, task_id in enumerate(task_ids):
626 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700627
628 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400629 shards_remaining = range(len(task_ids))
630 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700631 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700632 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700633 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634 shard_index, result = results_channel.pull(
635 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700636 except threading_utils.TaskChannel.Timeout:
637 if print_status_updates:
638 print(
639 'Waiting for results from the following shards: %s' %
640 ', '.join(map(str, shards_remaining)))
641 sys.stdout.flush()
642 continue
643 except Exception:
644 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700645
646 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700647 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000648 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500649 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700651
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652 # Yield back results to the caller.
653 assert shard_index in shards_remaining
654 shards_remaining.remove(shard_index)
655 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700656
maruel@chromium.org0437a732013-08-27 16:05:52 +0000657 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700658 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000659 should_stop.set()
660
661
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400662def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000663 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700664 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400665 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700666 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
667 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400668 else:
669 pending = 'N/A'
670
maruel77f720b2015-09-15 12:35:22 -0700671 if metadata.get('duration') is not None:
672 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400673 else:
674 duration = 'N/A'
675
maruel77f720b2015-09-15 12:35:22 -0700676 if metadata.get('exit_code') is not None:
677 # Integers are encoded as string to not loose precision.
678 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400679 else:
680 exit_code = 'N/A'
681
682 bot_id = metadata.get('bot_id') or 'N/A'
683
maruel77f720b2015-09-15 12:35:22 -0700684 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400685 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400686 tag_footer = (
687 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
688 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400689
690 tag_len = max(len(tag_header), len(tag_footer))
691 dash_pad = '+-%s-+\n' % ('-' * tag_len)
692 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
693 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
694
695 header = dash_pad + tag_header + dash_pad
696 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700697 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400698 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000699
700
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700701def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700702 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400703 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700704 """Retrieves results of a Swarming task.
705
706 Returns:
707 process exit code that should be returned to the user.
708 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700709 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700710 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700711
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700712 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700713 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400714 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700715 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400716 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400717 swarming, task_ids, timeout, None, print_status_updates,
718 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700719 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700720
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400721 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700722 shard_exit_code = metadata.get('exit_code')
723 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700724 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700725 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700726 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400727 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700728 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700729
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700730 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400731 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400732 if len(seen_shards) < len(task_ids):
733 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700734 else:
maruel77f720b2015-09-15 12:35:22 -0700735 print('%s: %s %s' % (
736 metadata.get('bot_id', 'N/A'),
737 metadata['task_id'],
738 shard_exit_code))
739 if metadata['output']:
740 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400741 if output:
742 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700743 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700744 summary = output_collector.finalize()
745 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700746 # TODO(maruel): Make this optional.
747 for i in summary['shards']:
748 if i:
749 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700750 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700751
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400752 if decorate and total_duration:
753 print('Total duration: %.1fs' % total_duration)
754
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400755 if len(seen_shards) != len(task_ids):
756 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700757 print >> sys.stderr, ('Results from some shards are missing: %s' %
758 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700759 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700760
maruela5490782015-09-30 10:56:59 -0700761 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000762
763
maruel77f720b2015-09-15 12:35:22 -0700764### API management.
765
766
767class APIError(Exception):
768 pass
769
770
771def endpoints_api_discovery_apis(host):
772 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
773 the APIs exposed by a host.
774
775 https://developers.google.com/discovery/v1/reference/apis/list
776 """
777 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
778 if data is None:
779 raise APIError('Failed to discover APIs on %s' % host)
780 out = {}
781 for api in data['items']:
782 if api['id'] == 'discovery:v1':
783 continue
784 # URL is of the following form:
785 # url = host + (
786 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
787 api_data = net.url_read_json(api['discoveryRestUrl'])
788 if api_data is None:
789 raise APIError('Failed to discover %s on %s' % (api['id'], host))
790 out[api['id']] = api_data
791 return out
792
793
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500794### Commands.
795
796
797def abort_task(_swarming, _manifest):
798 """Given a task manifest that was triggered, aborts its execution."""
799 # TODO(vadimsh): No supported by the server yet.
800
801
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400802def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400803 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500804 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500805 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500806 dest='dimensions', metavar='FOO bar',
807 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500808 parser.add_option_group(parser.filter_group)
809
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400810
Vadim Shtayurab450c602014-05-12 19:23:25 -0700811def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400812 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700813 parser.sharding_group.add_option(
814 '--shards', type='int', default=1,
815 help='Number of shards to trigger and collect.')
816 parser.add_option_group(parser.sharding_group)
817
818
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400819def add_trigger_options(parser):
820 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500821 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400822 add_filter_options(parser)
823
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400824 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500825 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500826 '-s', '--isolated',
827 help='Hash of the .isolated to grab from the isolate server')
828 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500829 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700830 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500831 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500832 '--priority', type='int', default=100,
833 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500834 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500835 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400836 help='Display name of the task. Defaults to '
837 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
838 'isolated file is provided, if a hash is provided, it defaults to '
839 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400840 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400841 '--tags', action='append', default=[],
842 help='Tags to assign to the task.')
843 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500844 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400845 help='User associated with the task. Defaults to authenticated user on '
846 'the server.')
847 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400848 '--idempotent', action='store_true', default=False,
849 help='When set, the server will actively try to find a previous task '
850 'with the same parameter and return this result instead if possible')
851 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400852 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400853 help='Seconds to allow the task to be pending for a bot to run before '
854 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400855 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400856 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400857 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400858 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400859 '--hard-timeout', type='int', default=60*60,
860 help='Seconds to allow the task to complete.')
861 parser.task_group.add_option(
862 '--io-timeout', type='int', default=20*60,
863 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500864 parser.task_group.add_option(
865 '--raw-cmd', action='store_true', default=False,
866 help='When set, the command after -- is used as-is without run_isolated. '
867 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500868 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000869
870
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500871def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500872 """Processes trigger options and uploads files to isolate server if necessary.
873 """
874 options.dimensions = dict(options.dimensions)
875 options.env = dict(options.env)
876
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500877 if not options.dimensions:
878 parser.error('Please at least specify one --dimension')
879 if options.raw_cmd:
880 if not args:
881 parser.error(
882 'Arguments with --raw-cmd should be passed after -- as command '
883 'delimiter.')
884 if options.isolate_server:
885 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
886
887 command = args
888 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500889 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500890 options.user,
891 '_'.join(
892 '%s=%s' % (k, v)
893 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700894 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500895 else:
896 isolateserver.process_isolate_server_options(parser, options, False)
897 try:
maruel77f720b2015-09-15 12:35:22 -0700898 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500899 except ValueError as e:
900 parser.error(str(e))
901
maruel77f720b2015-09-15 12:35:22 -0700902 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
903 # actual command to run.
904 properties = TaskProperties(
905 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500906 dimensions=options.dimensions,
907 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700908 execution_timeout_secs=options.hard_timeout,
909 extra_args=command if inputs_ref else None,
910 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500911 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700912 inputs_ref=inputs_ref,
913 io_timeout_secs=options.io_timeout)
914 return NewTaskRequest(
915 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500916 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700917 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500918 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700919 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500920 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700921 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000922
923
924def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500925 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000926 '-t', '--timeout',
927 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400928 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000929 help='Timeout to wait for result, set to 0 for no timeout; default: '
930 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500931 parser.group_logging.add_option(
932 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700933 parser.group_logging.add_option(
934 '--print-status-updates', action='store_true',
935 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400936 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700937 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700938 '--task-summary-json',
939 metavar='FILE',
940 help='Dump a summary of task results to this file as json. It contains '
941 'only shards statuses as know to server directly. Any output files '
942 'emitted by the task can be collected by using --task-output-dir')
943 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700944 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700945 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700946 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700947 'directory contains per-shard directory with output files produced '
948 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700949 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000950
951
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400952@subcommand.usage('bots...')
953def CMDbot_delete(parser, args):
954 """Forcibly deletes bots from the Swarming server."""
955 parser.add_option(
956 '-f', '--force', action='store_true',
957 help='Do not prompt for confirmation')
958 options, args = parser.parse_args(args)
959 if not args:
960 parser.error('Please specific bots to delete')
961
962 bots = sorted(args)
963 if not options.force:
964 print('Delete the following bots?')
965 for bot in bots:
966 print(' %s' % bot)
967 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
968 print('Goodbye.')
969 return 1
970
971 result = 0
972 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -0700973 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
974 if net.url_read_json(url, data={}, method='POST') is None:
975 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400976 result = 1
977 return result
978
979
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400980def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400981 """Returns information about the bots connected to the Swarming server."""
982 add_filter_options(parser)
983 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400984 '--dead-only', action='store_true',
985 help='Only print dead bots, useful to reap them and reimage broken bots')
986 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400987 '-k', '--keep-dead', action='store_true',
988 help='Do not filter out dead bots')
989 parser.filter_group.add_option(
990 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400991 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400992 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400993
994 if options.keep_dead and options.dead_only:
995 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700996
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400997 bots = []
998 cursor = None
999 limit = 250
1000 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001001 base_url = (
1002 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001003 while True:
1004 url = base_url
1005 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001006 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001007 data = net.url_read_json(url)
1008 if data is None:
1009 print >> sys.stderr, 'Failed to access %s' % options.swarming
1010 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001011 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001012 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001013 if not cursor:
1014 break
1015
maruel77f720b2015-09-15 12:35:22 -07001016 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001017 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001018 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001019 continue
maruel77f720b2015-09-15 12:35:22 -07001020 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001021 continue
1022
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001023 # If the user requested to filter on dimensions, ensure the bot has all the
1024 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001025 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001026 for key, value in options.dimensions:
1027 if key not in dimensions:
1028 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001029 # A bot can have multiple value for a key, for example,
1030 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1031 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001032 if isinstance(dimensions[key], list):
1033 if value not in dimensions[key]:
1034 break
1035 else:
1036 if value != dimensions[key]:
1037 break
1038 else:
maruel77f720b2015-09-15 12:35:22 -07001039 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001040 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001041 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001042 if bot.get('task_id'):
1043 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001044 return 0
1045
1046
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001047@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001048def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001049 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001050
1051 The result can be in multiple part if the execution was sharded. It can
1052 potentially have retries.
1053 """
1054 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001055 parser.add_option(
1056 '-j', '--json',
1057 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001058 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001059 if not args and not options.json:
1060 parser.error('Must specify at least one task id or --json.')
1061 if args and options.json:
1062 parser.error('Only use one of task id or --json.')
1063
1064 if options.json:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001065 try:
maruel12e30012015-10-09 11:55:35 -07001066 with fs.open(options.json, 'rb') as f:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001067 tasks = sorted(
1068 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1069 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001070 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001071 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001072 else:
1073 valid = frozenset('0123456789abcdef')
1074 if any(not valid.issuperset(task_id) for task_id in args):
1075 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001076
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001077 try:
1078 return collect(
1079 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001080 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001081 options.timeout,
1082 options.decorate,
1083 options.print_status_updates,
1084 options.task_summary_json,
1085 options.task_output_dir)
1086 except Failure:
1087 on_error.report(None)
1088 return 1
1089
1090
maruelbea00862015-09-18 09:55:36 -07001091@subcommand.usage('[filename]')
1092def CMDput_bootstrap(parser, args):
1093 """Uploads a new version of bootstrap.py."""
1094 options, args = parser.parse_args(args)
1095 if len(args) != 1:
1096 parser.error('Must specify file to upload')
1097 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel12e30012015-10-09 11:55:35 -07001098 with fs.open(args[0], 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001099 content = f.read().decode('utf-8')
1100 data = net.url_read_json(url, data={'content': content})
1101 print data
1102 return 0
1103
1104
1105@subcommand.usage('[filename]')
1106def CMDput_bot_config(parser, args):
1107 """Uploads a new version of bot_config.py."""
1108 options, args = parser.parse_args(args)
1109 if len(args) != 1:
1110 parser.error('Must specify file to upload')
1111 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel12e30012015-10-09 11:55:35 -07001112 with fs.open(args[0], 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001113 content = f.read().decode('utf-8')
1114 data = net.url_read_json(url, data={'content': content})
1115 print data
1116 return 0
1117
1118
maruel77f720b2015-09-15 12:35:22 -07001119@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001120def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001121 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1122 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001123
1124 Examples:
maruel77f720b2015-09-15 12:35:22 -07001125 Listing all bots:
1126 swarming.py query -S https://server-url bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001127
maruel77f720b2015-09-15 12:35:22 -07001128 Listing last 10 tasks on a specific bot named 'swarm1':
1129 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001130 """
1131 CHUNK_SIZE = 250
1132
1133 parser.add_option(
1134 '-L', '--limit', type='int', default=200,
1135 help='Limit to enforce on limitless items (like number of tasks); '
1136 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001137 parser.add_option(
1138 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001139 parser.add_option(
1140 '--progress', action='store_true',
1141 help='Prints a dot at each request to show progress')
1142 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001143 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001144 parser.error(
1145 'Must specify only method name and optionally query args properly '
1146 'escaped.')
1147 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001148 url = base_url
1149 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001150 # Check check, change if not working out.
1151 merge_char = '&' if '?' in url else '?'
1152 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001153 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001154 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001155 # TODO(maruel): Do basic diagnostic.
1156 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001157 return 1
1158
1159 # Some items support cursors. Try to get automatically if cursors are needed
1160 # by looking at the 'cursor' items.
1161 while (
1162 data.get('cursor') and
1163 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001164 merge_char = '&' if '?' in base_url else '?'
1165 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001166 if options.limit:
1167 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001168 if options.progress:
1169 sys.stdout.write('.')
1170 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001171 new = net.url_read_json(url)
1172 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001173 if options.progress:
1174 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001175 print >> sys.stderr, 'Failed to access %s' % options.swarming
1176 return 1
1177 data['items'].extend(new['items'])
maruel77f720b2015-09-15 12:35:22 -07001178 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001179
maruel77f720b2015-09-15 12:35:22 -07001180 if options.progress:
1181 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001182 if options.limit and len(data.get('items', [])) > options.limit:
1183 data['items'] = data['items'][:options.limit]
1184 data.pop('cursor', None)
1185
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001186 if options.json:
maruel77f720b2015-09-15 12:35:22 -07001187 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001188 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001189 try:
maruel77f720b2015-09-15 12:35:22 -07001190 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001191 sys.stdout.write('\n')
1192 except IOError:
1193 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001194 return 0
1195
1196
maruel77f720b2015-09-15 12:35:22 -07001197def CMDquery_list(parser, args):
1198 """Returns list of all the Swarming APIs that can be used with command
1199 'query'.
1200 """
1201 parser.add_option(
1202 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1203 options, args = parser.parse_args(args)
1204 if args:
1205 parser.error('No argument allowed.')
1206
1207 try:
1208 apis = endpoints_api_discovery_apis(options.swarming)
1209 except APIError as e:
1210 parser.error(str(e))
1211 if options.json:
maruel12e30012015-10-09 11:55:35 -07001212 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001213 json.dump(apis, f)
1214 else:
1215 help_url = (
1216 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1217 options.swarming)
1218 for api_id, api in sorted(apis.iteritems()):
1219 print api_id
1220 print ' ' + api['description']
1221 for resource_name, resource in sorted(api['resources'].iteritems()):
1222 print ''
1223 for method_name, method in sorted(resource['methods'].iteritems()):
1224 # Only list the GET ones.
1225 if method['httpMethod'] != 'GET':
1226 continue
1227 print '- %s.%s: %s' % (
1228 resource_name, method_name, method['path'])
1229 print ' ' + method['description']
1230 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1231 return 0
1232
1233
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001234@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001235def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001236 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001237
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001238 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001239 """
1240 add_trigger_options(parser)
1241 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001242 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001243 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001244 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001245 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001246 tasks = trigger_task_shards(
1247 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001248 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001249 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001250 'Failed to trigger %s(%s): %s' %
1251 (options.task_name, args[0], e.args[0]))
1252 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001253 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001254 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001255 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001256 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001257 task_ids = [
1258 t['task_id']
1259 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1260 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001261 try:
1262 return collect(
1263 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001264 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001265 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001266 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001267 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001268 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001269 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001270 except Failure:
1271 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001272 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001273
1274
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001275@subcommand.usage('task_id')
1276def CMDreproduce(parser, args):
1277 """Runs a task locally that was triggered on the server.
1278
1279 This running locally the same commands that have been run on the bot. The data
1280 downloaded will be in a subdirectory named 'work' of the current working
1281 directory.
1282 """
1283 options, args = parser.parse_args(args)
1284 if len(args) != 1:
1285 parser.error('Must specify exactly one task id.')
1286
maruel77f720b2015-09-15 12:35:22 -07001287 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001288 request = net.url_read_json(url)
1289 if not request:
1290 print >> sys.stderr, 'Failed to retrieve request data for the task'
1291 return 1
1292
maruel12e30012015-10-09 11:55:35 -07001293 workdir = unicode(os.path.abspath('work'))
1294 if not fs.isdir(workdir):
1295 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001296
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001297 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001298 env = None
1299 if properties['env']:
1300 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001301 logging.info('env: %r', properties['env'])
1302 env.update(
maruel77f720b2015-09-15 12:35:22 -07001303 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1304 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001305
maruel77f720b2015-09-15 12:35:22 -07001306 try:
maruel12e30012015-10-09 11:55:35 -07001307 return subprocess.call(properties['command'], env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001308 except OSError as e:
1309 print >> sys.stderr, 'Failed to run: %s' % ' '.join(properties['command'])
1310 print >> sys.stderr, str(e)
1311 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001312
1313
maruel0eb1d1b2015-10-02 14:48:21 -07001314@subcommand.usage('bot_id')
1315def CMDterminate(parser, args):
1316 """Tells a bot to gracefully shut itself down as soon as it can.
1317
1318 This is done by completing whatever current task there is then exiting the bot
1319 process.
1320 """
1321 parser.add_option(
1322 '--wait', action='store_true', help='Wait for the bot to terminate')
1323 options, args = parser.parse_args(args)
1324 if len(args) != 1:
1325 parser.error('Please provide the bot id')
1326 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1327 request = net.url_read_json(url, data={})
1328 if not request:
1329 print >> sys.stderr, 'Failed to ask for termination'
1330 return 1
1331 if options.wait:
1332 return collect(
1333 options.swarming, [request['task_id']], 0., False, False, None, None)
1334 return 0
1335
1336
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001337@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001338def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001339 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001340
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001341 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001342 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001343
1344 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001345
1346 Passes all extra arguments provided after '--' as additional command line
1347 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001348 """
1349 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001350 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001351 parser.add_option(
1352 '--dump-json',
1353 metavar='FILE',
1354 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001355 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001356 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001357 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001358 tasks = trigger_task_shards(
1359 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001360 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001361 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001362 tasks_sorted = sorted(
1363 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001364 if options.dump_json:
1365 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001366 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001367 'tasks': tasks,
1368 }
1369 tools.write_json(options.dump_json, data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001370 print('To collect results, use:')
1371 print(' swarming.py collect -S %s --json %s' %
1372 (options.swarming, options.dump_json))
1373 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001374 print('To collect results, use:')
1375 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001376 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1377 print('Or visit:')
1378 for t in tasks_sorted:
1379 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001380 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001381 except Failure:
1382 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001383 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001384
1385
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001386class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001387 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001388 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001389 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001390 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001391 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001392 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001393 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001394 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001395 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001396 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001397
1398 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001399 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001400 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001401 auth.process_auth_options(self, options)
1402 user = self._process_swarming(options)
1403 if hasattr(options, 'user') and not options.user:
1404 options.user = user
1405 return options, args
1406
1407 def _process_swarming(self, options):
1408 """Processes the --swarming option and aborts if not specified.
1409
1410 Returns the identity as determined by the server.
1411 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001412 if not options.swarming:
1413 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001414 try:
1415 options.swarming = net.fix_url(options.swarming)
1416 except ValueError as e:
1417 self.error('--swarming %s' % e)
1418 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001419 try:
1420 user = auth.ensure_logged_in(options.swarming)
1421 except ValueError as e:
1422 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001423 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001424
1425
1426def main(args):
1427 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001428 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001429
1430
1431if __name__ == '__main__':
1432 fix_encoding.fix_encoding()
1433 tools.disable_buffering()
1434 colorama.init()
1435 sys.exit(main(sys.argv[1:]))