blob: dfc64ee5fd80ea98bb010ef8b5ffc60f160e9780 [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2013 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005
6"""Client tool to trigger tasks or retrieve results from a Swarming server."""
7
maruelb76604c2015-11-11 11:53:44 -08008__version__ = '0.8.4'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import subprocess
17import sys
maruel29ab2fd2015-10-16 11:44:01 -070018import tempfile
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel@chromium.org0437a732013-08-27 16:05:52 +000033from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000034from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000035
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040037import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000038import isolateserver
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'):
maruele7dc3872015-10-16 11:51:40 -070059 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070060 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050061 if not file_hash:
62 on_error.report('Archival failure %s' % arg)
63 return None, True
64 return file_hash, True
65 elif isolated_format.is_valid_hash(arg, algo):
66 return arg, False
67 else:
68 on_error.report('Invalid hash %s' % arg)
69 return None, False
70
71
72def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050073 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050074
75 Returns:
maruel77f720b2015-09-15 12:35:22 -070076 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050077 """
78 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070079 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050080 if not options.isolated:
81 if '--' in args:
82 index = args.index('--')
83 isolated_cmd_args = args[index+1:]
84 args = args[:index]
85 else:
86 # optparse eats '--' sometimes.
87 isolated_cmd_args = args[1:]
88 args = args[:1]
89 if len(args) != 1:
90 raise ValueError(
91 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
92 'process.')
93 # Old code. To be removed eventually.
94 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070095 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050096 if not options.isolated:
97 raise ValueError('Invalid argument %s' % args[0])
98 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050099 if '--' in args:
100 index = args.index('--')
101 isolated_cmd_args = args[index+1:]
102 if index != 0:
103 raise ValueError('Unexpected arguments.')
104 else:
105 # optparse eats '--' sometimes.
106 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500107
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500108 # If a file name was passed, use its base name of the isolated hash.
109 # Otherwise, use user name as an approximation of a task name.
110 if not options.task_name:
111 if is_file:
112 key = os.path.splitext(os.path.basename(args[0]))[0]
113 else:
114 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500115 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500116 key,
117 '_'.join(
118 '%s=%s' % (k, v)
119 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500120 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500121
maruel77f720b2015-09-15 12:35:22 -0700122 inputs_ref = FilesRef(
123 isolated=options.isolated,
124 isolatedserver=options.isolate_server,
125 namespace=options.namespace)
126 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500127
128
129### Triggering.
130
131
maruel77f720b2015-09-15 12:35:22 -0700132# See ../appengine/swarming/swarming_rpcs.py.
133FilesRef = collections.namedtuple(
134 'FilesRef',
135 [
136 'isolated',
137 'isolatedserver',
138 'namespace',
139 ])
140
141
142# See ../appengine/swarming/swarming_rpcs.py.
143TaskProperties = collections.namedtuple(
144 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500145 [
146 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500147 'dimensions',
148 'env',
maruel77f720b2015-09-15 12:35:22 -0700149 'execution_timeout_secs',
150 'extra_args',
151 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500152 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700153 'inputs_ref',
154 'io_timeout_secs',
155 ])
156
157
158# See ../appengine/swarming/swarming_rpcs.py.
159NewTaskRequest = collections.namedtuple(
160 'NewTaskRequest',
161 [
162 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500163 'name',
maruel77f720b2015-09-15 12:35:22 -0700164 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500165 'priority',
maruel77f720b2015-09-15 12:35:22 -0700166 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500167 'tags',
168 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500169 ])
170
171
maruel77f720b2015-09-15 12:35:22 -0700172def namedtuple_to_dict(value):
173 """Recursively converts a namedtuple to a dict."""
174 out = dict(value._asdict())
175 for k, v in out.iteritems():
176 if hasattr(v, '_asdict'):
177 out[k] = namedtuple_to_dict(v)
178 return out
179
180
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500181def task_request_to_raw_request(task_request):
182 """Returns the json dict expected by the Swarming server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700183
184 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500185 """
maruel77f720b2015-09-15 12:35:22 -0700186 out = namedtuple_to_dict(task_request)
187 # Maps are not supported until protobuf v3.
188 out['properties']['dimensions'] = [
189 {'key': k, 'value': v}
190 for k, v in out['properties']['dimensions'].iteritems()
191 ]
192 out['properties']['dimensions'].sort(key=lambda x: x['key'])
193 out['properties']['env'] = [
194 {'key': k, 'value': v}
195 for k, v in out['properties']['env'].iteritems()
196 ]
197 out['properties']['env'].sort(key=lambda x: x['key'])
198 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199
200
maruel77f720b2015-09-15 12:35:22 -0700201def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500202 """Triggers a request on the Swarming server and returns the json data.
203
204 It's the low-level function.
205
206 Returns:
207 {
208 'request': {
209 'created_ts': u'2010-01-02 03:04:05',
210 'name': ..
211 },
212 'task_id': '12300',
213 }
214 """
215 logging.info('Triggering: %s', raw_request['name'])
216
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500217 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700218 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500219 if not result:
220 on_error.report('Failed to trigger task %s' % raw_request['name'])
221 return None
maruele557bce2015-11-17 09:01:27 -0800222 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800223 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800224 msg = 'Failed to trigger task %s' % raw_request['name']
225 if result['error'].get('errors'):
226 for err in result['error']['errors']:
227 if err.get('message'):
228 msg += '\nMessage: %s' % err['message']
229 if err.get('debugInfo'):
230 msg += '\nDebug info:\n%s' % err['debugInfo']
231 elif result['error'].get('message'):
232 msg += '\nMessage: %s' % result['message']
233
234 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800235 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500236 return result
237
238
239def setup_googletest(env, shards, index):
240 """Sets googletest specific environment variables."""
241 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700242 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
243 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
244 env = env[:]
245 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
246 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500247 return env
248
249
250def trigger_task_shards(swarming, task_request, shards):
251 """Triggers one or many subtasks of a sharded task.
252
253 Returns:
254 Dict with task details, returned to caller as part of --dump-json output.
255 None in case of failure.
256 """
257 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700258 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500259 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700260 req['properties']['env'] = setup_googletest(
261 req['properties']['env'], shards, index)
262 req['name'] += ':%s:%s' % (index, shards)
263 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500264
265 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500266 tasks = {}
267 priority_warning = False
268 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700269 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500270 if not task:
271 break
272 logging.info('Request result: %s', task)
273 if (not priority_warning and
274 task['request']['priority'] != task_request.priority):
275 priority_warning = True
276 print >> sys.stderr, (
277 'Priority was reset to %s' % task['request']['priority'])
278 tasks[request['name']] = {
279 'shard_index': index,
280 'task_id': task['task_id'],
281 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
282 }
283
284 # Some shards weren't triggered. Abort everything.
285 if len(tasks) != len(requests):
286 if tasks:
287 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
288 len(tasks), len(requests))
289 for task_dict in tasks.itervalues():
290 abort_task(swarming, task_dict['task_id'])
291 return None
292
293 return tasks
294
295
296### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000297
298
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700299# How often to print status updates to stdout in 'collect'.
300STATUS_UPDATE_INTERVAL = 15 * 60.
301
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400302
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400303class State(object):
304 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000305
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400306 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
307 values are part of the API so if they change, the API changed.
308
309 It's in fact an enum. Values should be in decreasing order of importance.
310 """
311 RUNNING = 0x10
312 PENDING = 0x20
313 EXPIRED = 0x30
314 TIMED_OUT = 0x40
315 BOT_DIED = 0x50
316 CANCELED = 0x60
317 COMPLETED = 0x70
318
maruel77f720b2015-09-15 12:35:22 -0700319 STATES = (
320 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
321 'COMPLETED')
322 STATES_RUNNING = ('RUNNING', 'PENDING')
323 STATES_NOT_RUNNING = (
324 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
325 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
326 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400327
328 _NAMES = {
329 RUNNING: 'Running',
330 PENDING: 'Pending',
331 EXPIRED: 'Expired',
332 TIMED_OUT: 'Execution timed out',
333 BOT_DIED: 'Bot died',
334 CANCELED: 'User canceled',
335 COMPLETED: 'Completed',
336 }
337
maruel77f720b2015-09-15 12:35:22 -0700338 _ENUMS = {
339 'RUNNING': RUNNING,
340 'PENDING': PENDING,
341 'EXPIRED': EXPIRED,
342 'TIMED_OUT': TIMED_OUT,
343 'BOT_DIED': BOT_DIED,
344 'CANCELED': CANCELED,
345 'COMPLETED': COMPLETED,
346 }
347
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400348 @classmethod
349 def to_string(cls, state):
350 """Returns a user-readable string representing a State."""
351 if state not in cls._NAMES:
352 raise ValueError('Invalid state %s' % state)
353 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000354
maruel77f720b2015-09-15 12:35:22 -0700355 @classmethod
356 def from_enum(cls, state):
357 """Returns int value based on the string."""
358 if state not in cls._ENUMS:
359 raise ValueError('Invalid state %s' % state)
360 return cls._ENUMS[state]
361
maruel@chromium.org0437a732013-08-27 16:05:52 +0000362
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700364 """Assembles task execution summary (for --task-summary-json output).
365
366 Optionally fetches task outputs from isolate server to local disk (used when
367 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368
369 This object is shared among multiple threads running 'retrieve_results'
370 function, in particular they call 'process_shard_result' method in parallel.
371 """
372
maruel0eb1d1b2015-10-02 14:48:21 -0700373 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
375
376 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700377 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700378 shard_count: expected number of task shards.
379 """
maruel12e30012015-10-09 11:55:35 -0700380 self.task_output_dir = (
381 unicode(os.path.abspath(task_output_dir))
382 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700383 self.shard_count = shard_count
384
385 self._lock = threading.Lock()
386 self._per_shard_results = {}
387 self._storage = None
388
maruel12e30012015-10-09 11:55:35 -0700389 if self.task_output_dir and not fs.isdir(self.task_output_dir):
390 fs.makedirs(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700391
Vadim Shtayurab450c602014-05-12 19:23:25 -0700392 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700393 """Stores results of a single task shard, fetches output files if necessary.
394
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400395 Modifies |result| in place.
396
maruel77f720b2015-09-15 12:35:22 -0700397 shard_index is 0-based.
398
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399 Called concurrently from multiple threads.
400 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700401 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700402 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700403 if shard_index < 0 or shard_index >= self.shard_count:
404 logging.warning(
405 'Shard index %d is outside of expected range: [0; %d]',
406 shard_index, self.shard_count - 1)
407 return
408
maruel77f720b2015-09-15 12:35:22 -0700409 if result.get('outputs_ref'):
410 ref = result['outputs_ref']
411 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
412 ref['isolatedserver'],
413 urllib.urlencode(
414 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400415
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700416 # Store result dict of that shard, ignore results we've already seen.
417 with self._lock:
418 if shard_index in self._per_shard_results:
419 logging.warning('Ignoring duplicate shard index %d', shard_index)
420 return
421 self._per_shard_results[shard_index] = result
422
423 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700424 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400425 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700426 result['outputs_ref']['isolatedserver'],
427 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400428 if storage:
429 # Output files are supposed to be small and they are not reused across
430 # tasks. So use MemoryCache for them instead of on-disk cache. Make
431 # files writable, so that calling script can delete them.
432 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700433 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400434 storage,
435 isolateserver.MemoryCache(file_mode_mask=0700),
436 os.path.join(self.task_output_dir, str(shard_index)),
437 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438
439 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441 with self._lock:
442 # Write an array of shard results with None for missing shards.
443 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444 'shards': [
445 self._per_shard_results.get(i) for i in xrange(self.shard_count)
446 ],
447 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700448 # Write summary.json to task_output_dir as well.
449 if self.task_output_dir:
450 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700451 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700452 summary,
453 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 if self._storage:
455 self._storage.close()
456 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700457 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700458
459 def _get_storage(self, isolate_server, namespace):
460 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700461 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700462 with self._lock:
463 if not self._storage:
464 self._storage = isolateserver.get_storage(isolate_server, namespace)
465 else:
466 # Shards must all use exact same isolate server and namespace.
467 if self._storage.location != isolate_server:
468 logging.error(
469 'Task shards are using multiple isolate servers: %s and %s',
470 self._storage.location, isolate_server)
471 return None
472 if self._storage.namespace != namespace:
473 logging.error(
474 'Task shards are using multiple namespaces: %s and %s',
475 self._storage.namespace, namespace)
476 return None
477 return self._storage
478
479
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500480def now():
481 """Exists so it can be mocked easily."""
482 return time.time()
483
484
maruel77f720b2015-09-15 12:35:22 -0700485def parse_time(value):
486 """Converts serialized time from the API to datetime.datetime."""
487 # When microseconds are 0, the '.123456' suffix is elided. This means the
488 # serialized format is not consistent, which confuses the hell out of python.
489 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
490 try:
491 return datetime.datetime.strptime(value, fmt)
492 except ValueError:
493 pass
494 raise ValueError('Failed to parse %s' % value)
495
496
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700497def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400498 base_url, shard_index, task_id, timeout, should_stop, output_collector):
499 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700500
Vadim Shtayurab450c602014-05-12 19:23:25 -0700501 Returns:
502 <result dict> on success.
503 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700504 """
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000505 assert isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700506 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
507 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700508 started = now()
509 deadline = started + timeout if timeout else None
510 attempt = 0
511
512 while not should_stop.is_set():
513 attempt += 1
514
515 # Waiting for too long -> give up.
516 current_time = now()
517 if deadline and current_time >= deadline:
518 logging.error('retrieve_results(%s) timed out on attempt %d',
519 base_url, attempt)
520 return None
521
522 # Do not spin too fast. Spin faster at the beginning though.
523 # Start with 1 sec delay and for each 30 sec of waiting add another second
524 # of delay, until hitting 15 sec ceiling.
525 if attempt > 1:
526 max_delay = min(15, 1 + (current_time - started) / 30.0)
527 delay = min(max_delay, deadline - current_time) if deadline else max_delay
528 if delay > 0:
529 logging.debug('Waiting %.1f sec before retrying', delay)
530 should_stop.wait(delay)
531 if should_stop.is_set():
532 return None
533
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400534 # Disable internal retries in net.url_read_json, since we are doing retries
535 # ourselves.
536 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700537 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
538 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400539 result = net.url_read_json(result_url, retry_50x=False)
540 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400541 continue
maruel77f720b2015-09-15 12:35:22 -0700542
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400543 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700544 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400545 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700546 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700547 # Record the result, try to fetch attached output files (if any).
548 if output_collector:
549 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700550 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700551 if result.get('internal_failure'):
552 logging.error('Internal error!')
553 elif result['state'] == 'BOT_DIED':
554 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700555 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000556
557
maruel77f720b2015-09-15 12:35:22 -0700558def convert_to_old_format(result):
559 """Converts the task result data from Endpoints API format to old API format
560 for compatibility.
561
562 This goes into the file generated as --task-summary-json.
563 """
564 # Sets default.
565 result.setdefault('abandoned_ts', None)
566 result.setdefault('bot_id', None)
567 result.setdefault('bot_version', None)
568 result.setdefault('children_task_ids', [])
569 result.setdefault('completed_ts', None)
570 result.setdefault('cost_saved_usd', None)
571 result.setdefault('costs_usd', None)
572 result.setdefault('deduped_from', None)
573 result.setdefault('name', None)
574 result.setdefault('outputs_ref', None)
575 result.setdefault('properties_hash', None)
576 result.setdefault('server_versions', None)
577 result.setdefault('started_ts', None)
578 result.setdefault('tags', None)
579 result.setdefault('user', None)
580
581 # Convertion back to old API.
582 duration = result.pop('duration', None)
583 result['durations'] = [duration] if duration else []
584 exit_code = result.pop('exit_code', None)
585 result['exit_codes'] = [int(exit_code)] if exit_code else []
586 result['id'] = result.pop('task_id')
587 result['isolated_out'] = result.get('outputs_ref', None)
588 output = result.pop('output', None)
589 result['outputs'] = [output] if output else []
590 # properties_hash
591 # server_version
592 # Endpoints result 'state' as string. For compatibility with old code, convert
593 # to int.
594 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700595 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700596 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700597 if 'bot_dimensions' in result:
598 result['bot_dimensions'] = {
599 i['key']: i['value'] for i in result['bot_dimensions']
600 }
601 else:
602 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700603
604
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700605def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400606 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
607 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500608 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000609
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700610 Duplicate shards are ignored. Shards are yielded in order of completion.
611 Timed out shards are NOT yielded at all. Caller can compare number of yielded
612 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000613
614 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500615 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 +0000616 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500617
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700618 output_collector is an optional instance of TaskOutputCollector that will be
619 used to fetch files produced by a task from isolate server to the local disk.
620
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500621 Yields:
622 (index, result). In particular, 'result' is defined as the
623 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400626 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700627 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700628 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700629
maruel@chromium.org0437a732013-08-27 16:05:52 +0000630 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
631 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700632 # Adds a task to the thread pool to call 'retrieve_results' and return
633 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400634 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700635 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000636 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400637 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
638 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639
640 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 for shard_index, task_id in enumerate(task_ids):
642 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643
644 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400645 shards_remaining = range(len(task_ids))
646 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700647 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700648 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 shard_index, result = results_channel.pull(
651 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700652 except threading_utils.TaskChannel.Timeout:
653 if print_status_updates:
654 print(
655 'Waiting for results from the following shards: %s' %
656 ', '.join(map(str, shards_remaining)))
657 sys.stdout.flush()
658 continue
659 except Exception:
660 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700661
662 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000664 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500665 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000666 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700667
Vadim Shtayurab450c602014-05-12 19:23:25 -0700668 # Yield back results to the caller.
669 assert shard_index in shards_remaining
670 shards_remaining.remove(shard_index)
671 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700672
maruel@chromium.org0437a732013-08-27 16:05:52 +0000673 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 should_stop.set()
676
677
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400678def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700680 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400681 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700682 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
683 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400684 else:
685 pending = 'N/A'
686
maruel77f720b2015-09-15 12:35:22 -0700687 if metadata.get('duration') is not None:
688 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400689 else:
690 duration = 'N/A'
691
maruel77f720b2015-09-15 12:35:22 -0700692 if metadata.get('exit_code') is not None:
693 # Integers are encoded as string to not loose precision.
694 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400695 else:
696 exit_code = 'N/A'
697
698 bot_id = metadata.get('bot_id') or 'N/A'
699
maruel77f720b2015-09-15 12:35:22 -0700700 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400701 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400702 tag_footer = (
703 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
704 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400705
706 tag_len = max(len(tag_header), len(tag_footer))
707 dash_pad = '+-%s-+\n' % ('-' * tag_len)
708 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
709 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
710
711 header = dash_pad + tag_header + dash_pad
712 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700713 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400714 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000715
716
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700717def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700718 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400719 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700720 """Retrieves results of a Swarming task.
721
722 Returns:
723 process exit code that should be returned to the user.
724 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700725 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700726 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700727
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700728 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700729 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400730 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400732 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400733 swarming, task_ids, timeout, None, print_status_updates,
734 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700735 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700736
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400737 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700738 shard_exit_code = metadata.get('exit_code')
739 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700740 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700741 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700742 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400743 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700744 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700745
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400747 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400748 if len(seen_shards) < len(task_ids):
749 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700750 else:
maruel77f720b2015-09-15 12:35:22 -0700751 print('%s: %s %s' % (
752 metadata.get('bot_id', 'N/A'),
753 metadata['task_id'],
754 shard_exit_code))
755 if metadata['output']:
756 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400757 if output:
758 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700759 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700760 summary = output_collector.finalize()
761 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700762 # TODO(maruel): Make this optional.
763 for i in summary['shards']:
764 if i:
765 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700766 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700767
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400768 if decorate and total_duration:
769 print('Total duration: %.1fs' % total_duration)
770
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400771 if len(seen_shards) != len(task_ids):
772 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700773 print >> sys.stderr, ('Results from some shards are missing: %s' %
774 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700775 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700776
maruela5490782015-09-30 10:56:59 -0700777 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000778
779
maruel77f720b2015-09-15 12:35:22 -0700780### API management.
781
782
783class APIError(Exception):
784 pass
785
786
787def endpoints_api_discovery_apis(host):
788 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
789 the APIs exposed by a host.
790
791 https://developers.google.com/discovery/v1/reference/apis/list
792 """
793 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
794 if data is None:
795 raise APIError('Failed to discover APIs on %s' % host)
796 out = {}
797 for api in data['items']:
798 if api['id'] == 'discovery:v1':
799 continue
800 # URL is of the following form:
801 # url = host + (
802 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
803 api_data = net.url_read_json(api['discoveryRestUrl'])
804 if api_data is None:
805 raise APIError('Failed to discover %s on %s' % (api['id'], host))
806 out[api['id']] = api_data
807 return out
808
809
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500810### Commands.
811
812
813def abort_task(_swarming, _manifest):
814 """Given a task manifest that was triggered, aborts its execution."""
815 # TODO(vadimsh): No supported by the server yet.
816
817
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400818def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400819 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500820 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500821 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500822 dest='dimensions', metavar='FOO bar',
823 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500824 parser.add_option_group(parser.filter_group)
825
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400826
Vadim Shtayurab450c602014-05-12 19:23:25 -0700827def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400828 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700829 parser.sharding_group.add_option(
830 '--shards', type='int', default=1,
831 help='Number of shards to trigger and collect.')
832 parser.add_option_group(parser.sharding_group)
833
834
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400835def add_trigger_options(parser):
836 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500837 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400838 add_filter_options(parser)
839
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400840 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500841 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500842 '-s', '--isolated',
843 help='Hash of the .isolated to grab from the isolate server')
844 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500845 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700846 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500847 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500848 '--priority', type='int', default=100,
849 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500850 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500851 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400852 help='Display name of the task. Defaults to '
853 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
854 'isolated file is provided, if a hash is provided, it defaults to '
855 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400856 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400857 '--tags', action='append', default=[],
858 help='Tags to assign to the task.')
859 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500860 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400861 help='User associated with the task. Defaults to authenticated user on '
862 'the server.')
863 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400864 '--idempotent', action='store_true', default=False,
865 help='When set, the server will actively try to find a previous task '
866 'with the same parameter and return this result instead if possible')
867 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400868 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400869 help='Seconds to allow the task to be pending for a bot to run before '
870 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400871 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400872 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400873 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400874 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400875 '--hard-timeout', type='int', default=60*60,
876 help='Seconds to allow the task to complete.')
877 parser.task_group.add_option(
878 '--io-timeout', type='int', default=20*60,
879 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500880 parser.task_group.add_option(
881 '--raw-cmd', action='store_true', default=False,
882 help='When set, the command after -- is used as-is without run_isolated. '
883 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500884 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000885
886
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500887def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500888 """Processes trigger options and uploads files to isolate server if necessary.
889 """
890 options.dimensions = dict(options.dimensions)
891 options.env = dict(options.env)
892
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500893 if not options.dimensions:
894 parser.error('Please at least specify one --dimension')
895 if options.raw_cmd:
896 if not args:
897 parser.error(
898 'Arguments with --raw-cmd should be passed after -- as command '
899 'delimiter.')
900 if options.isolate_server:
901 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
902
903 command = args
904 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500905 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500906 options.user,
907 '_'.join(
908 '%s=%s' % (k, v)
909 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700910 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500911 else:
912 isolateserver.process_isolate_server_options(parser, options, False)
913 try:
maruel77f720b2015-09-15 12:35:22 -0700914 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500915 except ValueError as e:
916 parser.error(str(e))
917
maruel77f720b2015-09-15 12:35:22 -0700918 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
919 # actual command to run.
920 properties = TaskProperties(
921 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500922 dimensions=options.dimensions,
923 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700924 execution_timeout_secs=options.hard_timeout,
925 extra_args=command if inputs_ref else None,
926 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500927 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700928 inputs_ref=inputs_ref,
929 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700930 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
931 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700932 return NewTaskRequest(
933 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500934 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700935 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500936 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700937 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500938 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700939 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000940
941
942def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500943 parser.server_group.add_option(
maruel@chromium.org0437a732013-08-27 16:05:52 +0000944 '-t', '--timeout',
945 type='float',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400946 default=80*60.,
maruel@chromium.org0437a732013-08-27 16:05:52 +0000947 help='Timeout to wait for result, set to 0 for no timeout; default: '
948 '%default s')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500949 parser.group_logging.add_option(
950 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700951 parser.group_logging.add_option(
952 '--print-status-updates', action='store_true',
953 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400954 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700955 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700956 '--task-summary-json',
957 metavar='FILE',
958 help='Dump a summary of task results to this file as json. It contains '
959 'only shards statuses as know to server directly. Any output files '
960 'emitted by the task can be collected by using --task-output-dir')
961 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700962 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700963 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700964 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700965 'directory contains per-shard directory with output files produced '
966 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700967 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000968
969
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400970@subcommand.usage('bots...')
971def CMDbot_delete(parser, args):
972 """Forcibly deletes bots from the Swarming server."""
973 parser.add_option(
974 '-f', '--force', action='store_true',
975 help='Do not prompt for confirmation')
976 options, args = parser.parse_args(args)
977 if not args:
978 parser.error('Please specific bots to delete')
979
980 bots = sorted(args)
981 if not options.force:
982 print('Delete the following bots?')
983 for bot in bots:
984 print(' %s' % bot)
985 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
986 print('Goodbye.')
987 return 1
988
989 result = 0
990 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -0700991 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
992 if net.url_read_json(url, data={}, method='POST') is None:
993 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400994 result = 1
995 return result
996
997
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400998def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400999 """Returns information about the bots connected to the Swarming server."""
1000 add_filter_options(parser)
1001 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001002 '--dead-only', action='store_true',
1003 help='Only print dead bots, useful to reap them and reimage broken bots')
1004 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001005 '-k', '--keep-dead', action='store_true',
1006 help='Do not filter out dead bots')
1007 parser.filter_group.add_option(
1008 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001009 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001010 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001011
1012 if options.keep_dead and options.dead_only:
1013 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001014
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001015 bots = []
1016 cursor = None
1017 limit = 250
1018 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001019 base_url = (
1020 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001021 while True:
1022 url = base_url
1023 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001024 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001025 data = net.url_read_json(url)
1026 if data is None:
1027 print >> sys.stderr, 'Failed to access %s' % options.swarming
1028 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001029 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001030 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001031 if not cursor:
1032 break
1033
maruel77f720b2015-09-15 12:35:22 -07001034 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001035 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001036 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001037 continue
maruel77f720b2015-09-15 12:35:22 -07001038 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001039 continue
1040
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001041 # If the user requested to filter on dimensions, ensure the bot has all the
1042 # dimensions requested.
maruel77f720b2015-09-15 12:35:22 -07001043 dimensions = {i['key']: i['value'] for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001044 for key, value in options.dimensions:
1045 if key not in dimensions:
1046 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001047 # A bot can have multiple value for a key, for example,
1048 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1049 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001050 if isinstance(dimensions[key], list):
1051 if value not in dimensions[key]:
1052 break
1053 else:
1054 if value != dimensions[key]:
1055 break
1056 else:
maruel77f720b2015-09-15 12:35:22 -07001057 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001058 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001059 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001060 if bot.get('task_id'):
1061 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001062 return 0
1063
1064
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001065@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001066def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001067 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001068
1069 The result can be in multiple part if the execution was sharded. It can
1070 potentially have retries.
1071 """
1072 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001073 parser.add_option(
1074 '-j', '--json',
1075 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001076 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001077 if not args and not options.json:
1078 parser.error('Must specify at least one task id or --json.')
1079 if args and options.json:
1080 parser.error('Only use one of task id or --json.')
1081
1082 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001083 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001084 try:
maruel1ceb3872015-10-14 06:10:44 -07001085 with fs.open(options.json, 'rb') as f:
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001086 tasks = sorted(
1087 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index'])
1088 args = [t['task_id'] for t in tasks]
Marc-Antoine Ruel5d055ed2015-04-22 14:59:56 -04001089 except (KeyError, IOError, TypeError, ValueError):
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001090 parser.error('Failed to parse %s' % options.json)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001091 else:
1092 valid = frozenset('0123456789abcdef')
1093 if any(not valid.issuperset(task_id) for task_id in args):
1094 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001095
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001096 try:
1097 return collect(
1098 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001099 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001100 options.timeout,
1101 options.decorate,
1102 options.print_status_updates,
1103 options.task_summary_json,
1104 options.task_output_dir)
1105 except Failure:
1106 on_error.report(None)
1107 return 1
1108
1109
maruelbea00862015-09-18 09:55:36 -07001110@subcommand.usage('[filename]')
1111def CMDput_bootstrap(parser, args):
1112 """Uploads a new version of bootstrap.py."""
1113 options, args = parser.parse_args(args)
1114 if len(args) != 1:
1115 parser.error('Must specify file to upload')
1116 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001117 path = unicode(os.path.abspath(args[0]))
1118 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001119 content = f.read().decode('utf-8')
1120 data = net.url_read_json(url, data={'content': content})
1121 print data
1122 return 0
1123
1124
1125@subcommand.usage('[filename]')
1126def CMDput_bot_config(parser, args):
1127 """Uploads a new version of bot_config.py."""
1128 options, args = parser.parse_args(args)
1129 if len(args) != 1:
1130 parser.error('Must specify file to upload')
1131 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001132 path = unicode(os.path.abspath(args[0]))
1133 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001134 content = f.read().decode('utf-8')
1135 data = net.url_read_json(url, data={'content': content})
1136 print data
1137 return 0
1138
1139
maruel77f720b2015-09-15 12:35:22 -07001140@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001141def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001142 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1143 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001144
1145 Examples:
maruel77f720b2015-09-15 12:35:22 -07001146 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001147 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001148
maruel77f720b2015-09-15 12:35:22 -07001149 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001150 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1151
1152 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1153 quoting is important!:
1154 swarming.py query -S server-url.com --limit 10 \\
1155 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001156 """
1157 CHUNK_SIZE = 250
1158
1159 parser.add_option(
1160 '-L', '--limit', type='int', default=200,
1161 help='Limit to enforce on limitless items (like number of tasks); '
1162 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001163 parser.add_option(
1164 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001165 parser.add_option(
1166 '--progress', action='store_true',
1167 help='Prints a dot at each request to show progress')
1168 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001169 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001170 parser.error(
1171 'Must specify only method name and optionally query args properly '
1172 'escaped.')
1173 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001174 url = base_url
1175 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001176 # Check check, change if not working out.
1177 merge_char = '&' if '?' in url else '?'
1178 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001179 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001180 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001181 # TODO(maruel): Do basic diagnostic.
1182 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001183 return 1
1184
1185 # Some items support cursors. Try to get automatically if cursors are needed
1186 # by looking at the 'cursor' items.
1187 while (
1188 data.get('cursor') and
1189 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001190 merge_char = '&' if '?' in base_url else '?'
1191 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001192 if options.limit:
1193 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001194 if options.progress:
1195 sys.stdout.write('.')
1196 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001197 new = net.url_read_json(url)
1198 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001199 if options.progress:
1200 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001201 print >> sys.stderr, 'Failed to access %s' % options.swarming
1202 return 1
maruel81b37132015-10-21 06:42:13 -07001203 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001204 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001205
maruel77f720b2015-09-15 12:35:22 -07001206 if options.progress:
1207 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001208 if options.limit and len(data.get('items', [])) > options.limit:
1209 data['items'] = data['items'][:options.limit]
1210 data.pop('cursor', None)
1211
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001212 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001213 options.json = unicode(os.path.abspath(options.json))
1214 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001215 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001216 try:
maruel77f720b2015-09-15 12:35:22 -07001217 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001218 sys.stdout.write('\n')
1219 except IOError:
1220 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001221 return 0
1222
1223
maruel77f720b2015-09-15 12:35:22 -07001224def CMDquery_list(parser, args):
1225 """Returns list of all the Swarming APIs that can be used with command
1226 'query'.
1227 """
1228 parser.add_option(
1229 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1230 options, args = parser.parse_args(args)
1231 if args:
1232 parser.error('No argument allowed.')
1233
1234 try:
1235 apis = endpoints_api_discovery_apis(options.swarming)
1236 except APIError as e:
1237 parser.error(str(e))
1238 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001239 options.json = unicode(os.path.abspath(options.json))
1240 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001241 json.dump(apis, f)
1242 else:
1243 help_url = (
1244 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1245 options.swarming)
1246 for api_id, api in sorted(apis.iteritems()):
1247 print api_id
1248 print ' ' + api['description']
1249 for resource_name, resource in sorted(api['resources'].iteritems()):
1250 print ''
1251 for method_name, method in sorted(resource['methods'].iteritems()):
1252 # Only list the GET ones.
1253 if method['httpMethod'] != 'GET':
1254 continue
1255 print '- %s.%s: %s' % (
1256 resource_name, method_name, method['path'])
1257 print ' ' + method['description']
1258 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1259 return 0
1260
1261
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001262@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001263def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001264 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001265
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001266 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001267 """
1268 add_trigger_options(parser)
1269 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001270 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001271 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001272 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001273 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001274 tasks = trigger_task_shards(
1275 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001276 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001277 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001278 'Failed to trigger %s(%s): %s' %
1279 (options.task_name, args[0], e.args[0]))
1280 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001281 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001282 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001283 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001284 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001285 task_ids = [
1286 t['task_id']
1287 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1288 ]
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001289 try:
1290 return collect(
1291 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001292 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001293 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001294 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001295 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001296 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001297 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001298 except Failure:
1299 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001300 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001301
1302
maruel18122c62015-10-23 06:31:23 -07001303@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001304def CMDreproduce(parser, args):
1305 """Runs a task locally that was triggered on the server.
1306
1307 This running locally the same commands that have been run on the bot. The data
1308 downloaded will be in a subdirectory named 'work' of the current working
1309 directory.
maruel18122c62015-10-23 06:31:23 -07001310
1311 You can pass further additional arguments to the target command by passing
1312 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001313 """
1314 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001315 extra_args = []
1316 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001317 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001318 if len(args) > 1:
1319 if args[1] == '--':
1320 if len(args) > 2:
1321 extra_args = args[2:]
1322 else:
1323 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001324
maruel77f720b2015-09-15 12:35:22 -07001325 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001326 request = net.url_read_json(url)
1327 if not request:
1328 print >> sys.stderr, 'Failed to retrieve request data for the task'
1329 return 1
1330
maruel12e30012015-10-09 11:55:35 -07001331 workdir = unicode(os.path.abspath('work'))
1332 if not fs.isdir(workdir):
1333 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001334
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001335 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001336 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001337 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001338 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001339 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001340 for i in properties['env']:
1341 key = i['key'].encode('utf-8')
1342 if not i['value']:
1343 env.pop(key, None)
1344 else:
1345 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001346
maruel29ab2fd2015-10-16 11:44:01 -07001347 if properties.get('inputs_ref'):
1348 # Create the tree.
1349 with isolateserver.get_storage(
1350 properties['inputs_ref']['isolatedserver'],
1351 properties['inputs_ref']['namespace']) as storage:
1352 bundle = isolateserver.fetch_isolated(
1353 properties['inputs_ref']['isolated'],
1354 storage,
1355 isolateserver.MemoryCache(file_mode_mask=0700),
1356 workdir,
1357 False)
1358 command = bundle.command
1359 if bundle.relative_cwd:
1360 workdir = os.path.join(workdir, bundle.relative_cwd)
1361 else:
1362 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001363 try:
maruel18122c62015-10-23 06:31:23 -07001364 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001365 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001366 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001367 print >> sys.stderr, str(e)
1368 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001369
1370
maruel0eb1d1b2015-10-02 14:48:21 -07001371@subcommand.usage('bot_id')
1372def CMDterminate(parser, args):
1373 """Tells a bot to gracefully shut itself down as soon as it can.
1374
1375 This is done by completing whatever current task there is then exiting the bot
1376 process.
1377 """
1378 parser.add_option(
1379 '--wait', action='store_true', help='Wait for the bot to terminate')
1380 options, args = parser.parse_args(args)
1381 if len(args) != 1:
1382 parser.error('Please provide the bot id')
1383 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1384 request = net.url_read_json(url, data={})
1385 if not request:
1386 print >> sys.stderr, 'Failed to ask for termination'
1387 return 1
1388 if options.wait:
1389 return collect(
1390 options.swarming, [request['task_id']], 0., False, False, None, None)
1391 return 0
1392
1393
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001394@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001395def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001396 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001397
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001398 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001399 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001400
1401 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001402
1403 Passes all extra arguments provided after '--' as additional command line
1404 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001405 """
1406 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001407 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001408 parser.add_option(
1409 '--dump-json',
1410 metavar='FILE',
1411 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001412 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001413 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001414 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001415 tasks = trigger_task_shards(
1416 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001417 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001418 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001419 tasks_sorted = sorted(
1420 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001421 if options.dump_json:
1422 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001423 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001424 'tasks': tasks,
1425 }
maruel46b015f2015-10-13 18:40:35 -07001426 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001427 print('To collect results, use:')
1428 print(' swarming.py collect -S %s --json %s' %
1429 (options.swarming, options.dump_json))
1430 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001431 print('To collect results, use:')
1432 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001433 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1434 print('Or visit:')
1435 for t in tasks_sorted:
1436 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001437 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001438 except Failure:
1439 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001440 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001441
1442
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001443class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001444 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001445 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001446 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001447 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001448 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001449 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001450 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001451 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001452 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001453 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001454
1455 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001456 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001457 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001458 auth.process_auth_options(self, options)
1459 user = self._process_swarming(options)
1460 if hasattr(options, 'user') and not options.user:
1461 options.user = user
1462 return options, args
1463
1464 def _process_swarming(self, options):
1465 """Processes the --swarming option and aborts if not specified.
1466
1467 Returns the identity as determined by the server.
1468 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001469 if not options.swarming:
1470 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001471 try:
1472 options.swarming = net.fix_url(options.swarming)
1473 except ValueError as e:
1474 self.error('--swarming %s' % e)
1475 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001476 try:
1477 user = auth.ensure_logged_in(options.swarming)
1478 except ValueError as e:
1479 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001480 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001481
1482
1483def main(args):
1484 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001485 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001486
1487
1488if __name__ == '__main__':
1489 fix_encoding.fix_encoding()
1490 tools.disable_buffering()
1491 colorama.init()
1492 sys.exit(main(sys.argv[1:]))