blob: 3e95d7a2e6495746d52c43e3f1ec00bddab5a851 [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
maruel29ab2fd2015-10-16 11:44:01 -07008__version__ = '0.8.3'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import subprocess
17import sys
maruel29ab2fd2015-10-16 11:44:01 -070018import tempfile
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel@chromium.org0437a732013-08-27 16:05:52 +000033from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000034from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000035
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040037import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000038import isolateserver
maruel@chromium.org0437a732013-08-27 16:05:52 +000039
40
41ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050042
43
44class Failure(Exception):
45 """Generic failure."""
46 pass
47
48
49### Isolated file handling.
50
51
maruel77f720b2015-09-15 12:35:22 -070052def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050053 """Archives a .isolated file if needed.
54
55 Returns the file hash to trigger and a bool specifying if it was a file (True)
56 or a hash (False).
57 """
58 if arg.endswith('.isolated'):
maruel77f720b2015-09-15 12:35:22 -070059 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050060 if not file_hash:
61 on_error.report('Archival failure %s' % arg)
62 return None, True
63 return file_hash, True
64 elif isolated_format.is_valid_hash(arg, algo):
65 return arg, False
66 else:
67 on_error.report('Invalid hash %s' % arg)
68 return None, False
69
70
71def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050072 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050073
74 Returns:
maruel77f720b2015-09-15 12:35:22 -070075 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050076 """
77 isolated_cmd_args = []
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050078 if not options.isolated:
79 if '--' in args:
80 index = args.index('--')
81 isolated_cmd_args = args[index+1:]
82 args = args[:index]
83 else:
84 # optparse eats '--' sometimes.
85 isolated_cmd_args = args[1:]
86 args = args[:1]
87 if len(args) != 1:
88 raise ValueError(
89 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
90 'process.')
91 # Old code. To be removed eventually.
92 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070093 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050094 if not options.isolated:
95 raise ValueError('Invalid argument %s' % args[0])
96 elif args:
97 is_file = False
98 if '--' in args:
99 index = args.index('--')
100 isolated_cmd_args = args[index+1:]
101 if index != 0:
102 raise ValueError('Unexpected arguments.')
103 else:
104 # optparse eats '--' sometimes.
105 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500106
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500107 # If a file name was passed, use its base name of the isolated hash.
108 # Otherwise, use user name as an approximation of a task name.
109 if not options.task_name:
110 if is_file:
111 key = os.path.splitext(os.path.basename(args[0]))[0]
112 else:
113 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500114 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500115 key,
116 '_'.join(
117 '%s=%s' % (k, v)
118 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500119 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500120
maruel77f720b2015-09-15 12:35:22 -0700121 inputs_ref = FilesRef(
122 isolated=options.isolated,
123 isolatedserver=options.isolate_server,
124 namespace=options.namespace)
125 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500126
127
128### Triggering.
129
130
maruel77f720b2015-09-15 12:35:22 -0700131# See ../appengine/swarming/swarming_rpcs.py.
132FilesRef = collections.namedtuple(
133 'FilesRef',
134 [
135 'isolated',
136 'isolatedserver',
137 'namespace',
138 ])
139
140
141# See ../appengine/swarming/swarming_rpcs.py.
142TaskProperties = collections.namedtuple(
143 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500144 [
145 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500146 'dimensions',
147 'env',
maruel77f720b2015-09-15 12:35:22 -0700148 'execution_timeout_secs',
149 'extra_args',
150 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500151 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700152 'inputs_ref',
153 'io_timeout_secs',
154 ])
155
156
157# See ../appengine/swarming/swarming_rpcs.py.
158NewTaskRequest = collections.namedtuple(
159 'NewTaskRequest',
160 [
161 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500162 'name',
maruel77f720b2015-09-15 12:35:22 -0700163 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 'priority',
maruel77f720b2015-09-15 12:35:22 -0700165 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500166 'tags',
167 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500168 ])
169
170
maruel77f720b2015-09-15 12:35:22 -0700171def namedtuple_to_dict(value):
172 """Recursively converts a namedtuple to a dict."""
173 out = dict(value._asdict())
174 for k, v in out.iteritems():
175 if hasattr(v, '_asdict'):
176 out[k] = namedtuple_to_dict(v)
177 return out
178
179
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500180def task_request_to_raw_request(task_request):
181 """Returns the json dict expected by the Swarming server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700182
183 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500184 """
maruel77f720b2015-09-15 12:35:22 -0700185 out = namedtuple_to_dict(task_request)
186 # Maps are not supported until protobuf v3.
187 out['properties']['dimensions'] = [
188 {'key': k, 'value': v}
189 for k, v in out['properties']['dimensions'].iteritems()
190 ]
191 out['properties']['dimensions'].sort(key=lambda x: x['key'])
192 out['properties']['env'] = [
193 {'key': k, 'value': v}
194 for k, v in out['properties']['env'].iteritems()
195 ]
196 out['properties']['env'].sort(key=lambda x: x['key'])
197 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500198
199
maruel77f720b2015-09-15 12:35:22 -0700200def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500201 """Triggers a request on the Swarming server and returns the json data.
202
203 It's the low-level function.
204
205 Returns:
206 {
207 'request': {
208 'created_ts': u'2010-01-02 03:04:05',
209 'name': ..
210 },
211 'task_id': '12300',
212 }
213 """
214 logging.info('Triggering: %s', raw_request['name'])
215
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500216 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700217 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500218 if not result:
219 on_error.report('Failed to trigger task %s' % raw_request['name'])
220 return None
221 return result
222
223
224def setup_googletest(env, shards, index):
225 """Sets googletest specific environment variables."""
226 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700227 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
228 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
229 env = env[:]
230 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
231 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500232 return env
233
234
235def trigger_task_shards(swarming, task_request, shards):
236 """Triggers one or many subtasks of a sharded task.
237
238 Returns:
239 Dict with task details, returned to caller as part of --dump-json output.
240 None in case of failure.
241 """
242 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700243 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500244 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700245 req['properties']['env'] = setup_googletest(
246 req['properties']['env'], shards, index)
247 req['name'] += ':%s:%s' % (index, shards)
248 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500249
250 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251 tasks = {}
252 priority_warning = False
253 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700254 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500255 if not task:
256 break
257 logging.info('Request result: %s', task)
258 if (not priority_warning and
259 task['request']['priority'] != task_request.priority):
260 priority_warning = True
261 print >> sys.stderr, (
262 'Priority was reset to %s' % task['request']['priority'])
263 tasks[request['name']] = {
264 'shard_index': index,
265 'task_id': task['task_id'],
266 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
267 }
268
269 # Some shards weren't triggered. Abort everything.
270 if len(tasks) != len(requests):
271 if tasks:
272 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
273 len(tasks), len(requests))
274 for task_dict in tasks.itervalues():
275 abort_task(swarming, task_dict['task_id'])
276 return None
277
278 return tasks
279
280
281### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000282
283
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700284# How often to print status updates to stdout in 'collect'.
285STATUS_UPDATE_INTERVAL = 15 * 60.
286
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400287
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400288class State(object):
289 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000290
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400291 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
292 values are part of the API so if they change, the API changed.
293
294 It's in fact an enum. Values should be in decreasing order of importance.
295 """
296 RUNNING = 0x10
297 PENDING = 0x20
298 EXPIRED = 0x30
299 TIMED_OUT = 0x40
300 BOT_DIED = 0x50
301 CANCELED = 0x60
302 COMPLETED = 0x70
303
maruel77f720b2015-09-15 12:35:22 -0700304 STATES = (
305 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
306 'COMPLETED')
307 STATES_RUNNING = ('RUNNING', 'PENDING')
308 STATES_NOT_RUNNING = (
309 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
310 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
311 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400312
313 _NAMES = {
314 RUNNING: 'Running',
315 PENDING: 'Pending',
316 EXPIRED: 'Expired',
317 TIMED_OUT: 'Execution timed out',
318 BOT_DIED: 'Bot died',
319 CANCELED: 'User canceled',
320 COMPLETED: 'Completed',
321 }
322
maruel77f720b2015-09-15 12:35:22 -0700323 _ENUMS = {
324 'RUNNING': RUNNING,
325 'PENDING': PENDING,
326 'EXPIRED': EXPIRED,
327 'TIMED_OUT': TIMED_OUT,
328 'BOT_DIED': BOT_DIED,
329 'CANCELED': CANCELED,
330 'COMPLETED': COMPLETED,
331 }
332
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400333 @classmethod
334 def to_string(cls, state):
335 """Returns a user-readable string representing a State."""
336 if state not in cls._NAMES:
337 raise ValueError('Invalid state %s' % state)
338 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000339
maruel77f720b2015-09-15 12:35:22 -0700340 @classmethod
341 def from_enum(cls, state):
342 """Returns int value based on the string."""
343 if state not in cls._ENUMS:
344 raise ValueError('Invalid state %s' % state)
345 return cls._ENUMS[state]
346
maruel@chromium.org0437a732013-08-27 16:05:52 +0000347
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700348class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700349 """Assembles task execution summary (for --task-summary-json output).
350
351 Optionally fetches task outputs from isolate server to local disk (used when
352 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353
354 This object is shared among multiple threads running 'retrieve_results'
355 function, in particular they call 'process_shard_result' method in parallel.
356 """
357
maruel0eb1d1b2015-10-02 14:48:21 -0700358 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700359 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
360
361 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700362 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 shard_count: expected number of task shards.
364 """
maruel12e30012015-10-09 11:55:35 -0700365 self.task_output_dir = (
366 unicode(os.path.abspath(task_output_dir))
367 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368 self.shard_count = shard_count
369
370 self._lock = threading.Lock()
371 self._per_shard_results = {}
372 self._storage = None
373
maruel12e30012015-10-09 11:55:35 -0700374 if self.task_output_dir and not fs.isdir(self.task_output_dir):
375 fs.makedirs(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376
Vadim Shtayurab450c602014-05-12 19:23:25 -0700377 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700378 """Stores results of a single task shard, fetches output files if necessary.
379
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400380 Modifies |result| in place.
381
maruel77f720b2015-09-15 12:35:22 -0700382 shard_index is 0-based.
383
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 Called concurrently from multiple threads.
385 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700387 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700388 if shard_index < 0 or shard_index >= self.shard_count:
389 logging.warning(
390 'Shard index %d is outside of expected range: [0; %d]',
391 shard_index, self.shard_count - 1)
392 return
393
maruel77f720b2015-09-15 12:35:22 -0700394 if result.get('outputs_ref'):
395 ref = result['outputs_ref']
396 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
397 ref['isolatedserver'],
398 urllib.urlencode(
399 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400400
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700401 # Store result dict of that shard, ignore results we've already seen.
402 with self._lock:
403 if shard_index in self._per_shard_results:
404 logging.warning('Ignoring duplicate shard index %d', shard_index)
405 return
406 self._per_shard_results[shard_index] = result
407
408 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700409 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400410 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700411 result['outputs_ref']['isolatedserver'],
412 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400413 if storage:
414 # Output files are supposed to be small and they are not reused across
415 # tasks. So use MemoryCache for them instead of on-disk cache. Make
416 # files writable, so that calling script can delete them.
417 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700418 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400419 storage,
420 isolateserver.MemoryCache(file_mode_mask=0700),
421 os.path.join(self.task_output_dir, str(shard_index)),
422 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700423
424 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700425 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700426 with self._lock:
427 # Write an array of shard results with None for missing shards.
428 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700429 'shards': [
430 self._per_shard_results.get(i) for i in xrange(self.shard_count)
431 ],
432 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700433 # Write summary.json to task_output_dir as well.
434 if self.task_output_dir:
435 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700436 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700437 summary,
438 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439 if self._storage:
440 self._storage.close()
441 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443
444 def _get_storage(self, isolate_server, namespace):
445 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700446 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447 with self._lock:
448 if not self._storage:
449 self._storage = isolateserver.get_storage(isolate_server, namespace)
450 else:
451 # Shards must all use exact same isolate server and namespace.
452 if self._storage.location != isolate_server:
453 logging.error(
454 'Task shards are using multiple isolate servers: %s and %s',
455 self._storage.location, isolate_server)
456 return None
457 if self._storage.namespace != namespace:
458 logging.error(
459 'Task shards are using multiple namespaces: %s and %s',
460 self._storage.namespace, namespace)
461 return None
462 return self._storage
463
464
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500465def now():
466 """Exists so it can be mocked easily."""
467 return time.time()
468
469
maruel77f720b2015-09-15 12:35:22 -0700470def parse_time(value):
471 """Converts serialized time from the API to datetime.datetime."""
472 # When microseconds are 0, the '.123456' suffix is elided. This means the
473 # serialized format is not consistent, which confuses the hell out of python.
474 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
475 try:
476 return datetime.datetime.strptime(value, fmt)
477 except ValueError:
478 pass
479 raise ValueError('Failed to parse %s' % value)
480
481
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700482def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400483 base_url, shard_index, task_id, timeout, should_stop, output_collector):
484 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700485
Vadim Shtayurab450c602014-05-12 19:23:25 -0700486 Returns:
487 <result dict> on success.
488 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700489 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000490 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700491 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
492 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700493 started = now()
494 deadline = started + timeout if timeout else None
495 attempt = 0
496
497 while not should_stop.is_set():
498 attempt += 1
499
500 # Waiting for too long -> give up.
501 current_time = now()
502 if deadline and current_time >= deadline:
503 logging.error('retrieve_results(%s) timed out on attempt %d',
504 base_url, attempt)
505 return None
506
507 # Do not spin too fast. Spin faster at the beginning though.
508 # Start with 1 sec delay and for each 30 sec of waiting add another second
509 # of delay, until hitting 15 sec ceiling.
510 if attempt > 1:
511 max_delay = min(15, 1 + (current_time - started) / 30.0)
512 delay = min(max_delay, deadline - current_time) if deadline else max_delay
513 if delay > 0:
514 logging.debug('Waiting %.1f sec before retrying', delay)
515 should_stop.wait(delay)
516 if should_stop.is_set():
517 return None
518
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400519 # Disable internal retries in net.url_read_json, since we are doing retries
520 # ourselves.
521 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700522 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
523 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400524 result = net.url_read_json(result_url, retry_50x=False)
525 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400526 continue
maruel77f720b2015-09-15 12:35:22 -0700527
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400528 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700529 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400530 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700531 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700532 # Record the result, try to fetch attached output files (if any).
533 if output_collector:
534 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700535 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700536 if result.get('internal_failure'):
537 logging.error('Internal error!')
538 elif result['state'] == 'BOT_DIED':
539 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700540 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000541
542
maruel77f720b2015-09-15 12:35:22 -0700543def convert_to_old_format(result):
544 """Converts the task result data from Endpoints API format to old API format
545 for compatibility.
546
547 This goes into the file generated as --task-summary-json.
548 """
549 # Sets default.
550 result.setdefault('abandoned_ts', None)
551 result.setdefault('bot_id', None)
552 result.setdefault('bot_version', None)
553 result.setdefault('children_task_ids', [])
554 result.setdefault('completed_ts', None)
555 result.setdefault('cost_saved_usd', None)
556 result.setdefault('costs_usd', None)
557 result.setdefault('deduped_from', None)
558 result.setdefault('name', None)
559 result.setdefault('outputs_ref', None)
560 result.setdefault('properties_hash', None)
561 result.setdefault('server_versions', None)
562 result.setdefault('started_ts', None)
563 result.setdefault('tags', None)
564 result.setdefault('user', None)
565
566 # Convertion back to old API.
567 duration = result.pop('duration', None)
568 result['durations'] = [duration] if duration else []
569 exit_code = result.pop('exit_code', None)
570 result['exit_codes'] = [int(exit_code)] if exit_code else []
571 result['id'] = result.pop('task_id')
572 result['isolated_out'] = result.get('outputs_ref', None)
573 output = result.pop('output', None)
574 result['outputs'] = [output] if output else []
575 # properties_hash
576 # server_version
577 # Endpoints result 'state' as string. For compatibility with old code, convert
578 # to int.
579 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700580 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700581 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700582 if 'bot_dimensions' in result:
583 result['bot_dimensions'] = {
584 i['key']: i['value'] for i in result['bot_dimensions']
585 }
586 else:
587 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700588
589
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700590def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400591 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
592 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500593 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000594
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700595 Duplicate shards are ignored. Shards are yielded in order of completion.
596 Timed out shards are NOT yielded at all. Caller can compare number of yielded
597 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000598
599 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500600 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 +0000601 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500602
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700603 output_collector is an optional instance of TaskOutputCollector that will be
604 used to fetch files produced by a task from isolate server to the local disk.
605
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500606 Yields:
607 (index, result). In particular, 'result' is defined as the
608 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000609 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000610 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400611 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700612 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700613 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700614
maruel@chromium.org0437a732013-08-27 16:05:52 +0000615 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
616 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700617 # Adds a task to the thread pool to call 'retrieve_results' and return
618 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400619 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700620 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000621 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400622 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
623 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700624
625 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400626 for shard_index, task_id in enumerate(task_ids):
627 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700628
629 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400630 shards_remaining = range(len(task_ids))
631 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700632 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700633 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700634 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700635 shard_index, result = results_channel.pull(
636 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700637 except threading_utils.TaskChannel.Timeout:
638 if print_status_updates:
639 print(
640 'Waiting for results from the following shards: %s' %
641 ', '.join(map(str, shards_remaining)))
642 sys.stdout.flush()
643 continue
644 except Exception:
645 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700646
647 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700648 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000649 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500650 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000651 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700652
Vadim Shtayurab450c602014-05-12 19:23:25 -0700653 # Yield back results to the caller.
654 assert shard_index in shards_remaining
655 shards_remaining.remove(shard_index)
656 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700657
maruel@chromium.org0437a732013-08-27 16:05:52 +0000658 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700659 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000660 should_stop.set()
661
662
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400663def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000664 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700665 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400666 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700667 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
668 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400669 else:
670 pending = 'N/A'
671
maruel77f720b2015-09-15 12:35:22 -0700672 if metadata.get('duration') is not None:
673 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400674 else:
675 duration = 'N/A'
676
maruel77f720b2015-09-15 12:35:22 -0700677 if metadata.get('exit_code') is not None:
678 # Integers are encoded as string to not loose precision.
679 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400680 else:
681 exit_code = 'N/A'
682
683 bot_id = metadata.get('bot_id') or 'N/A'
684
maruel77f720b2015-09-15 12:35:22 -0700685 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400686 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400687 tag_footer = (
688 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
689 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400690
691 tag_len = max(len(tag_header), len(tag_footer))
692 dash_pad = '+-%s-+\n' % ('-' * tag_len)
693 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
694 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
695
696 header = dash_pad + tag_header + dash_pad
697 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700698 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400699 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000700
701
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700702def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700703 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400704 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700705 """Retrieves results of a Swarming task.
706
707 Returns:
708 process exit code that should be returned to the user.
709 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700710 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700711 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700712
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700713 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700714 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400715 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700716 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400717 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400718 swarming, task_ids, timeout, None, print_status_updates,
719 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700720 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700721
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400722 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700723 shard_exit_code = metadata.get('exit_code')
724 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700725 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700726 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700727 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400728 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700729 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700730
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400732 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400733 if len(seen_shards) < len(task_ids):
734 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700735 else:
maruel77f720b2015-09-15 12:35:22 -0700736 print('%s: %s %s' % (
737 metadata.get('bot_id', 'N/A'),
738 metadata['task_id'],
739 shard_exit_code))
740 if metadata['output']:
741 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400742 if output:
743 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700744 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700745 summary = output_collector.finalize()
746 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700747 # TODO(maruel): Make this optional.
748 for i in summary['shards']:
749 if i:
750 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700751 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700752
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400753 if decorate and total_duration:
754 print('Total duration: %.1fs' % total_duration)
755
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400756 if len(seen_shards) != len(task_ids):
757 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700758 print >> sys.stderr, ('Results from some shards are missing: %s' %
759 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700760 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700761
maruela5490782015-09-30 10:56:59 -0700762 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000763
764
maruel77f720b2015-09-15 12:35:22 -0700765### API management.
766
767
768class APIError(Exception):
769 pass
770
771
772def endpoints_api_discovery_apis(host):
773 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
774 the APIs exposed by a host.
775
776 https://developers.google.com/discovery/v1/reference/apis/list
777 """
778 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
779 if data is None:
780 raise APIError('Failed to discover APIs on %s' % host)
781 out = {}
782 for api in data['items']:
783 if api['id'] == 'discovery:v1':
784 continue
785 # URL is of the following form:
786 # url = host + (
787 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
788 api_data = net.url_read_json(api['discoveryRestUrl'])
789 if api_data is None:
790 raise APIError('Failed to discover %s on %s' % (api['id'], host))
791 out[api['id']] = api_data
792 return out
793
794
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500795### Commands.
796
797
798def abort_task(_swarming, _manifest):
799 """Given a task manifest that was triggered, aborts its execution."""
800 # TODO(vadimsh): No supported by the server yet.
801
802
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400803def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400804 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500805 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500806 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500807 dest='dimensions', metavar='FOO bar',
808 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500809 parser.add_option_group(parser.filter_group)
810
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400811
Vadim Shtayurab450c602014-05-12 19:23:25 -0700812def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400813 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700814 parser.sharding_group.add_option(
815 '--shards', type='int', default=1,
816 help='Number of shards to trigger and collect.')
817 parser.add_option_group(parser.sharding_group)
818
819
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400820def add_trigger_options(parser):
821 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500822 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400823 add_filter_options(parser)
824
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400825 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500826 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500827 '-s', '--isolated',
828 help='Hash of the .isolated to grab from the isolate server')
829 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500830 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700831 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500832 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500833 '--priority', type='int', default=100,
834 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500835 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500836 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400837 help='Display name of the task. Defaults to '
838 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
839 'isolated file is provided, if a hash is provided, it defaults to '
840 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400841 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400842 '--tags', action='append', default=[],
843 help='Tags to assign to the task.')
844 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500845 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400846 help='User associated with the task. Defaults to authenticated user on '
847 'the server.')
848 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400849 '--idempotent', action='store_true', default=False,
850 help='When set, the server will actively try to find a previous task '
851 'with the same parameter and return this result instead if possible')
852 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400853 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400854 help='Seconds to allow the task to be pending for a bot to run before '
855 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400856 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400857 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400858 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400859 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400860 '--hard-timeout', type='int', default=60*60,
861 help='Seconds to allow the task to complete.')
862 parser.task_group.add_option(
863 '--io-timeout', type='int', default=20*60,
864 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500865 parser.task_group.add_option(
866 '--raw-cmd', action='store_true', default=False,
867 help='When set, the command after -- is used as-is without run_isolated. '
868 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500869 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000870
871
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500872def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500873 """Processes trigger options and uploads files to isolate server if necessary.
874 """
875 options.dimensions = dict(options.dimensions)
876 options.env = dict(options.env)
877
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500878 if not options.dimensions:
879 parser.error('Please at least specify one --dimension')
880 if options.raw_cmd:
881 if not args:
882 parser.error(
883 'Arguments with --raw-cmd should be passed after -- as command '
884 'delimiter.')
885 if options.isolate_server:
886 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
887
888 command = args
889 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500890 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500891 options.user,
892 '_'.join(
893 '%s=%s' % (k, v)
894 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700895 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500896 else:
897 isolateserver.process_isolate_server_options(parser, options, False)
898 try:
maruel77f720b2015-09-15 12:35:22 -0700899 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500900 except ValueError as e:
901 parser.error(str(e))
902
maruel77f720b2015-09-15 12:35:22 -0700903 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
904 # actual command to run.
905 properties = TaskProperties(
906 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500907 dimensions=options.dimensions,
908 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700909 execution_timeout_secs=options.hard_timeout,
910 extra_args=command if inputs_ref else None,
911 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500912 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700913 inputs_ref=inputs_ref,
914 io_timeout_secs=options.io_timeout)
915 return NewTaskRequest(
916 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500917 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700918 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500919 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700920 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500921 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700922 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000923
924
925def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500926 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000927 '-t', '--timeout',
928 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400929 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000930 help='Timeout to wait for result, set to 0 for no timeout; default: '
931 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500932 parser.group_logging.add_option(
933 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700934 parser.group_logging.add_option(
935 '--print-status-updates', action='store_true',
936 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400937 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700938 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700939 '--task-summary-json',
940 metavar='FILE',
941 help='Dump a summary of task results to this file as json. It contains '
942 'only shards statuses as know to server directly. Any output files '
943 'emitted by the task can be collected by using --task-output-dir')
944 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700945 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700946 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700947 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700948 'directory contains per-shard directory with output files produced '
949 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700950 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000951
952
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400953@subcommand.usage('bots...')
954def CMDbot_delete(parser, args):
955 """Forcibly deletes bots from the Swarming server."""
956 parser.add_option(
957 '-f', '--force', action='store_true',
958 help='Do not prompt for confirmation')
959 options, args = parser.parse_args(args)
960 if not args:
961 parser.error('Please specific bots to delete')
962
963 bots = sorted(args)
964 if not options.force:
965 print('Delete the following bots?')
966 for bot in bots:
967 print(' %s' % bot)
968 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
969 print('Goodbye.')
970 return 1
971
972 result = 0
973 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -0700974 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
975 if net.url_read_json(url, data={}, method='POST') is None:
976 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400977 result = 1
978 return result
979
980
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400981def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400982 """Returns information about the bots connected to the Swarming server."""
983 add_filter_options(parser)
984 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400985 '--dead-only', action='store_true',
986 help='Only print dead bots, useful to reap them and reimage broken bots')
987 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400988 '-k', '--keep-dead', action='store_true',
989 help='Do not filter out dead bots')
990 parser.filter_group.add_option(
991 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -0400992 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400993 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -0400994
995 if options.keep_dead and options.dead_only:
996 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700997
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -0400998 bots = []
999 cursor = None
1000 limit = 250
1001 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001002 base_url = (
1003 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001004 while True:
1005 url = base_url
1006 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001007 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001008 data = net.url_read_json(url)
1009 if data is None:
1010 print >> sys.stderr, 'Failed to access %s' % options.swarming
1011 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001012 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001013 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001014 if not cursor:
1015 break
1016
maruel77f720b2015-09-15 12:35:22 -07001017 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001018 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001019 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001020 continue
maruel77f720b2015-09-15 12:35:22 -07001021 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001022 continue
1023
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001024 # If the user requested to filter on dimensions, ensure the bot has all the
1025 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001026 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001027 for key, value in options.dimensions:
1028 if key not in dimensions:
1029 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001030 # A bot can have multiple value for a key, for example,
1031 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1032 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001033 if isinstance(dimensions[key], list):
1034 if value not in dimensions[key]:
1035 break
1036 else:
1037 if value != dimensions[key]:
1038 break
1039 else:
maruel77f720b2015-09-15 12:35:22 -07001040 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001041 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001042 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001043 if bot.get('task_id'):
1044 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001045 return 0
1046
1047
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001048@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001049def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001050 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001051
1052 The result can be in multiple part if the execution was sharded. It can
1053 potentially have retries.
1054 """
1055 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001056 parser.add_option(
1057 '-j', '--json',
1058 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001059 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001060 if not args and not options.json:
1061 parser.error('Must specify at least one task id or --json.')
1062 if args and options.json:
1063 parser.error('Only use one of task id or --json.')
1064
1065 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001066 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001067 try:
maruel1ceb3872015-10-14 06:10:44 -07001068 with fs.open(options.json, 'rb') as f:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001069 tasks = sorted(
1070 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1071 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001072 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001073 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001074 else:
1075 valid = frozenset('0123456789abcdef')
1076 if any(not valid.issuperset(task_id) for task_id in args):
1077 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001078
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001079 try:
1080 return collect(
1081 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001082 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001083 options.timeout,
1084 options.decorate,
1085 options.print_status_updates,
1086 options.task_summary_json,
1087 options.task_output_dir)
1088 except Failure:
1089 on_error.report(None)
1090 return 1
1091
1092
maruelbea00862015-09-18 09:55:36 -07001093@subcommand.usage('[filename]')
1094def CMDput_bootstrap(parser, args):
1095 """Uploads a new version of bootstrap.py."""
1096 options, args = parser.parse_args(args)
1097 if len(args) != 1:
1098 parser.error('Must specify file to upload')
1099 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001100 path = unicode(os.path.abspath(args[0]))
1101 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001102 content = f.read().decode('utf-8')
1103 data = net.url_read_json(url, data={'content': content})
1104 print data
1105 return 0
1106
1107
1108@subcommand.usage('[filename]')
1109def CMDput_bot_config(parser, args):
1110 """Uploads a new version of bot_config.py."""
1111 options, args = parser.parse_args(args)
1112 if len(args) != 1:
1113 parser.error('Must specify file to upload')
1114 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001115 path = unicode(os.path.abspath(args[0]))
1116 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001117 content = f.read().decode('utf-8')
1118 data = net.url_read_json(url, data={'content': content})
1119 print data
1120 return 0
1121
1122
maruel77f720b2015-09-15 12:35:22 -07001123@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001124def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001125 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1126 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001127
1128 Examples:
maruel77f720b2015-09-15 12:35:22 -07001129 Listing all bots:
1130 swarming.py query -S https://server-url bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001131
maruel77f720b2015-09-15 12:35:22 -07001132 Listing last 10 tasks on a specific bot named 'swarm1':
1133 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001134 """
1135 CHUNK_SIZE = 250
1136
1137 parser.add_option(
1138 '-L', '--limit', type='int', default=200,
1139 help='Limit to enforce on limitless items (like number of tasks); '
1140 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001141 parser.add_option(
1142 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001143 parser.add_option(
1144 '--progress', action='store_true',
1145 help='Prints a dot at each request to show progress')
1146 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001147 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001148 parser.error(
1149 'Must specify only method name and optionally query args properly '
1150 'escaped.')
1151 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001152 url = base_url
1153 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001154 # Check check, change if not working out.
1155 merge_char = '&' if '?' in url else '?'
1156 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001157 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001158 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001159 # TODO(maruel): Do basic diagnostic.
1160 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001161 return 1
1162
1163 # Some items support cursors. Try to get automatically if cursors are needed
1164 # by looking at the 'cursor' items.
1165 while (
1166 data.get('cursor') and
1167 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001168 merge_char = '&' if '?' in base_url else '?'
1169 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001170 if options.limit:
1171 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001172 if options.progress:
1173 sys.stdout.write('.')
1174 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001175 new = net.url_read_json(url)
1176 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001177 if options.progress:
1178 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001179 print >> sys.stderr, 'Failed to access %s' % options.swarming
1180 return 1
1181 data['items'].extend(new['items'])
maruel77f720b2015-09-15 12:35:22 -07001182 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001183
maruel77f720b2015-09-15 12:35:22 -07001184 if options.progress:
1185 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001186 if options.limit and len(data.get('items', [])) > options.limit:
1187 data['items'] = data['items'][:options.limit]
1188 data.pop('cursor', None)
1189
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001190 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001191 options.json = unicode(os.path.abspath(options.json))
1192 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001193 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001194 try:
maruel77f720b2015-09-15 12:35:22 -07001195 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001196 sys.stdout.write('\n')
1197 except IOError:
1198 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001199 return 0
1200
1201
maruel77f720b2015-09-15 12:35:22 -07001202def CMDquery_list(parser, args):
1203 """Returns list of all the Swarming APIs that can be used with command
1204 'query'.
1205 """
1206 parser.add_option(
1207 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1208 options, args = parser.parse_args(args)
1209 if args:
1210 parser.error('No argument allowed.')
1211
1212 try:
1213 apis = endpoints_api_discovery_apis(options.swarming)
1214 except APIError as e:
1215 parser.error(str(e))
1216 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001217 options.json = unicode(os.path.abspath(options.json))
1218 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001219 json.dump(apis, f)
1220 else:
1221 help_url = (
1222 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1223 options.swarming)
1224 for api_id, api in sorted(apis.iteritems()):
1225 print api_id
1226 print ' ' + api['description']
1227 for resource_name, resource in sorted(api['resources'].iteritems()):
1228 print ''
1229 for method_name, method in sorted(resource['methods'].iteritems()):
1230 # Only list the GET ones.
1231 if method['httpMethod'] != 'GET':
1232 continue
1233 print '- %s.%s: %s' % (
1234 resource_name, method_name, method['path'])
1235 print ' ' + method['description']
1236 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1237 return 0
1238
1239
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001240@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001241def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001242 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001243
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001244 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001245 """
1246 add_trigger_options(parser)
1247 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001248 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001249 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001250 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001251 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001252 tasks = trigger_task_shards(
1253 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001254 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001255 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001256 'Failed to trigger %s(%s): %s' %
1257 (options.task_name, args[0], e.args[0]))
1258 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001259 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001260 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001261 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001262 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001263 task_ids = [
1264 t['task_id']
1265 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1266 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001267 try:
1268 return collect(
1269 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001270 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001271 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001272 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001273 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001274 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001275 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001276 except Failure:
1277 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001278 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001279
1280
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001281@subcommand.usage('task_id')
1282def CMDreproduce(parser, args):
1283 """Runs a task locally that was triggered on the server.
1284
1285 This running locally the same commands that have been run on the bot. The data
1286 downloaded will be in a subdirectory named 'work' of the current working
1287 directory.
1288 """
1289 options, args = parser.parse_args(args)
1290 if len(args) != 1:
1291 parser.error('Must specify exactly one task id.')
1292
maruel77f720b2015-09-15 12:35:22 -07001293 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001294 request = net.url_read_json(url)
1295 if not request:
1296 print >> sys.stderr, 'Failed to retrieve request data for the task'
1297 return 1
1298
maruel12e30012015-10-09 11:55:35 -07001299 workdir = unicode(os.path.abspath('work'))
1300 if not fs.isdir(workdir):
1301 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001302
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001303 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001304 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001305 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001306 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001307 logging.info('env: %r', properties['env'])
1308 env.update(
maruel77f720b2015-09-15 12:35:22 -07001309 (i['key'].encode('utf-8'), i['value'].encode('utf-8'))
1310 for i in properties['env'])
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001311
maruel29ab2fd2015-10-16 11:44:01 -07001312 if properties.get('inputs_ref'):
1313 # Create the tree.
1314 with isolateserver.get_storage(
1315 properties['inputs_ref']['isolatedserver'],
1316 properties['inputs_ref']['namespace']) as storage:
1317 bundle = isolateserver.fetch_isolated(
1318 properties['inputs_ref']['isolated'],
1319 storage,
1320 isolateserver.MemoryCache(file_mode_mask=0700),
1321 workdir,
1322 False)
1323 command = bundle.command
1324 if bundle.relative_cwd:
1325 workdir = os.path.join(workdir, bundle.relative_cwd)
1326 else:
1327 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001328 try:
maruel29ab2fd2015-10-16 11:44:01 -07001329 return subprocess.call(command, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001330 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001331 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001332 print >> sys.stderr, str(e)
1333 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001334
1335
maruel0eb1d1b2015-10-02 14:48:21 -07001336@subcommand.usage('bot_id')
1337def CMDterminate(parser, args):
1338 """Tells a bot to gracefully shut itself down as soon as it can.
1339
1340 This is done by completing whatever current task there is then exiting the bot
1341 process.
1342 """
1343 parser.add_option(
1344 '--wait', action='store_true', help='Wait for the bot to terminate')
1345 options, args = parser.parse_args(args)
1346 if len(args) != 1:
1347 parser.error('Please provide the bot id')
1348 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1349 request = net.url_read_json(url, data={})
1350 if not request:
1351 print >> sys.stderr, 'Failed to ask for termination'
1352 return 1
1353 if options.wait:
1354 return collect(
1355 options.swarming, [request['task_id']], 0., False, False, None, None)
1356 return 0
1357
1358
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001359@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001360def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001361 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001362
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001363 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001364 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001365
1366 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001367
1368 Passes all extra arguments provided after '--' as additional command line
1369 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001370 """
1371 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001372 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001373 parser.add_option(
1374 '--dump-json',
1375 metavar='FILE',
1376 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001377 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001378 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001379 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001380 tasks = trigger_task_shards(
1381 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001382 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001383 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001384 tasks_sorted = sorted(
1385 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001386 if options.dump_json:
1387 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001388 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001389 'tasks': tasks,
1390 }
maruel46b015f2015-10-13 18:40:35 -07001391 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001392 print('To collect results, use:')
1393 print(' swarming.py collect -S %s --json %s' %
1394 (options.swarming, options.dump_json))
1395 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001396 print('To collect results, use:')
1397 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001398 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1399 print('Or visit:')
1400 for t in tasks_sorted:
1401 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001402 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001403 except Failure:
1404 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001405 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001406
1407
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001408class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001409 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001410 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001411 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001412 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001413 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001414 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001415 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001416 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001417 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001418 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001419
1420 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001421 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001422 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001423 auth.process_auth_options(self, options)
1424 user = self._process_swarming(options)
1425 if hasattr(options, 'user') and not options.user:
1426 options.user = user
1427 return options, args
1428
1429 def _process_swarming(self, options):
1430 """Processes the --swarming option and aborts if not specified.
1431
1432 Returns the identity as determined by the server.
1433 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001434 if not options.swarming:
1435 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001436 try:
1437 options.swarming = net.fix_url(options.swarming)
1438 except ValueError as e:
1439 self.error('--swarming %s' % e)
1440 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001441 try:
1442 user = auth.ensure_logged_in(options.swarming)
1443 except ValueError as e:
1444 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001445 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001446
1447
1448def main(args):
1449 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001450 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001451
1452
1453if __name__ == '__main__':
1454 fix_encoding.fix_encoding()
1455 tools.disable_buffering()
1456 colorama.init()
1457 sys.exit(main(sys.argv[1:]))