blob: 2fead3ed37345d5af91a34367e0bd8d25cd4f7cd [file] [log] [blame]
maruel@chromium.org0437a732013-08-27 16:05:52 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00005
6"""Client tool to trigger tasks or retrieve results from a Swarming server."""
7
maruel9531ce02016-04-13 06:11:23 -07008__version__ = '0.8.5'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
maruel@chromium.org0437a732013-08-27 16:05:52 +000016import subprocess
17import sys
maruel29ab2fd2015-10-16 11:44:01 -070018import tempfile
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040038import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
maruelc070e672016-02-22 17:32:57 -080040import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000041
42
43ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050044
45
46class Failure(Exception):
47 """Generic failure."""
48 pass
49
50
51### Isolated file handling.
52
53
maruel77f720b2015-09-15 12:35:22 -070054def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050055 """Archives a .isolated file if needed.
56
57 Returns the file hash to trigger and a bool specifying if it was a file (True)
58 or a hash (False).
59 """
60 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070061 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070062 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050063 if not file_hash:
64 on_error.report('Archival failure %s' % arg)
65 return None, True
66 return file_hash, True
67 elif isolated_format.is_valid_hash(arg, algo):
68 return arg, False
69 else:
70 on_error.report('Invalid hash %s' % arg)
71 return None, False
72
73
74def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050075 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050076
77 Returns:
maruel77f720b2015-09-15 12:35:22 -070078 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050079 """
80 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070081 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050082 if not options.isolated:
83 if '--' in args:
84 index = args.index('--')
85 isolated_cmd_args = args[index+1:]
86 args = args[:index]
87 else:
88 # optparse eats '--' sometimes.
89 isolated_cmd_args = args[1:]
90 args = args[:1]
91 if len(args) != 1:
92 raise ValueError(
93 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
94 'process.')
95 # Old code. To be removed eventually.
96 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070097 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050098 if not options.isolated:
99 raise ValueError('Invalid argument %s' % args[0])
100 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500101 if '--' in args:
102 index = args.index('--')
103 isolated_cmd_args = args[index+1:]
104 if index != 0:
105 raise ValueError('Unexpected arguments.')
106 else:
107 # optparse eats '--' sometimes.
108 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500109
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110 # If a file name was passed, use its base name of the isolated hash.
111 # Otherwise, use user name as an approximation of a task name.
112 if not options.task_name:
113 if is_file:
114 key = os.path.splitext(os.path.basename(args[0]))[0]
115 else:
116 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500117 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500118 key,
119 '_'.join(
120 '%s=%s' % (k, v)
121 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500122 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500123
maruel77f720b2015-09-15 12:35:22 -0700124 inputs_ref = FilesRef(
nodir152cba62016-05-12 16:08:56 -0700125 isolated=options.isolated,
126 isolatedserver=options.isolate_server,
127 namespace=options.namespace)
maruel77f720b2015-09-15 12:35:22 -0700128 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500129
130
131### Triggering.
132
133
maruel77f720b2015-09-15 12:35:22 -0700134# See ../appengine/swarming/swarming_rpcs.py.
135FilesRef = collections.namedtuple(
136 'FilesRef',
137 [
138 'isolated',
139 'isolatedserver',
140 'namespace',
141 ])
142
143
144# See ../appengine/swarming/swarming_rpcs.py.
145TaskProperties = collections.namedtuple(
146 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500147 [
148 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500149 'dimensions',
150 'env',
maruel77f720b2015-09-15 12:35:22 -0700151 'execution_timeout_secs',
152 'extra_args',
153 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500154 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700155 'inputs_ref',
156 'io_timeout_secs',
157 ])
158
159
160# See ../appengine/swarming/swarming_rpcs.py.
161NewTaskRequest = collections.namedtuple(
162 'NewTaskRequest',
163 [
164 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500165 'name',
maruel77f720b2015-09-15 12:35:22 -0700166 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500167 'priority',
maruel77f720b2015-09-15 12:35:22 -0700168 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500169 'tags',
170 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 ])
172
173
maruel77f720b2015-09-15 12:35:22 -0700174def namedtuple_to_dict(value):
175 """Recursively converts a namedtuple to a dict."""
176 out = dict(value._asdict())
177 for k, v in out.iteritems():
178 if hasattr(v, '_asdict'):
179 out[k] = namedtuple_to_dict(v)
180 return out
181
182
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500183def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800184 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700185
186 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500187 """
maruel77f720b2015-09-15 12:35:22 -0700188 out = namedtuple_to_dict(task_request)
189 # Maps are not supported until protobuf v3.
190 out['properties']['dimensions'] = [
191 {'key': k, 'value': v}
192 for k, v in out['properties']['dimensions'].iteritems()
193 ]
194 out['properties']['dimensions'].sort(key=lambda x: x['key'])
195 out['properties']['env'] = [
196 {'key': k, 'value': v}
197 for k, v in out['properties']['env'].iteritems()
198 ]
199 out['properties']['env'].sort(key=lambda x: x['key'])
200 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500201
202
maruel77f720b2015-09-15 12:35:22 -0700203def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500204 """Triggers a request on the Swarming server and returns the json data.
205
206 It's the low-level function.
207
208 Returns:
209 {
210 'request': {
211 'created_ts': u'2010-01-02 03:04:05',
212 'name': ..
213 },
214 'task_id': '12300',
215 }
216 """
217 logging.info('Triggering: %s', raw_request['name'])
218
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500219 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700220 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500221 if not result:
222 on_error.report('Failed to trigger task %s' % raw_request['name'])
223 return None
maruele557bce2015-11-17 09:01:27 -0800224 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800225 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800226 msg = 'Failed to trigger task %s' % raw_request['name']
227 if result['error'].get('errors'):
228 for err in result['error']['errors']:
229 if err.get('message'):
230 msg += '\nMessage: %s' % err['message']
231 if err.get('debugInfo'):
232 msg += '\nDebug info:\n%s' % err['debugInfo']
233 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800234 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800235
236 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800237 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500238 return result
239
240
241def setup_googletest(env, shards, index):
242 """Sets googletest specific environment variables."""
243 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700244 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
245 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
246 env = env[:]
247 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
248 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500249 return env
250
251
252def trigger_task_shards(swarming, task_request, shards):
253 """Triggers one or many subtasks of a sharded task.
254
255 Returns:
256 Dict with task details, returned to caller as part of --dump-json output.
257 None in case of failure.
258 """
259 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700260 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500261 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700262 req['properties']['env'] = setup_googletest(
263 req['properties']['env'], shards, index)
264 req['name'] += ':%s:%s' % (index, shards)
265 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500266
267 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500268 tasks = {}
269 priority_warning = False
270 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700271 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500272 if not task:
273 break
274 logging.info('Request result: %s', task)
275 if (not priority_warning and
276 task['request']['priority'] != task_request.priority):
277 priority_warning = True
278 print >> sys.stderr, (
279 'Priority was reset to %s' % task['request']['priority'])
280 tasks[request['name']] = {
281 'shard_index': index,
282 'task_id': task['task_id'],
283 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
284 }
285
286 # Some shards weren't triggered. Abort everything.
287 if len(tasks) != len(requests):
288 if tasks:
289 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
290 len(tasks), len(requests))
291 for task_dict in tasks.itervalues():
292 abort_task(swarming, task_dict['task_id'])
293 return None
294
295 return tasks
296
297
298### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000299
300
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700301# How often to print status updates to stdout in 'collect'.
302STATUS_UPDATE_INTERVAL = 15 * 60.
303
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400304
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400305class State(object):
306 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000307
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400308 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
309 values are part of the API so if they change, the API changed.
310
311 It's in fact an enum. Values should be in decreasing order of importance.
312 """
313 RUNNING = 0x10
314 PENDING = 0x20
315 EXPIRED = 0x30
316 TIMED_OUT = 0x40
317 BOT_DIED = 0x50
318 CANCELED = 0x60
319 COMPLETED = 0x70
320
maruel77f720b2015-09-15 12:35:22 -0700321 STATES = (
322 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
323 'COMPLETED')
324 STATES_RUNNING = ('RUNNING', 'PENDING')
325 STATES_NOT_RUNNING = (
326 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
327 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
328 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400329
330 _NAMES = {
331 RUNNING: 'Running',
332 PENDING: 'Pending',
333 EXPIRED: 'Expired',
334 TIMED_OUT: 'Execution timed out',
335 BOT_DIED: 'Bot died',
336 CANCELED: 'User canceled',
337 COMPLETED: 'Completed',
338 }
339
maruel77f720b2015-09-15 12:35:22 -0700340 _ENUMS = {
341 'RUNNING': RUNNING,
342 'PENDING': PENDING,
343 'EXPIRED': EXPIRED,
344 'TIMED_OUT': TIMED_OUT,
345 'BOT_DIED': BOT_DIED,
346 'CANCELED': CANCELED,
347 'COMPLETED': COMPLETED,
348 }
349
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400350 @classmethod
351 def to_string(cls, state):
352 """Returns a user-readable string representing a State."""
353 if state not in cls._NAMES:
354 raise ValueError('Invalid state %s' % state)
355 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000356
maruel77f720b2015-09-15 12:35:22 -0700357 @classmethod
358 def from_enum(cls, state):
359 """Returns int value based on the string."""
360 if state not in cls._ENUMS:
361 raise ValueError('Invalid state %s' % state)
362 return cls._ENUMS[state]
363
maruel@chromium.org0437a732013-08-27 16:05:52 +0000364
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700365class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700366 """Assembles task execution summary (for --task-summary-json output).
367
368 Optionally fetches task outputs from isolate server to local disk (used when
369 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370
371 This object is shared among multiple threads running 'retrieve_results'
372 function, in particular they call 'process_shard_result' method in parallel.
373 """
374
maruel0eb1d1b2015-10-02 14:48:21 -0700375 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
377
378 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700379 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700380 shard_count: expected number of task shards.
381 """
maruel12e30012015-10-09 11:55:35 -0700382 self.task_output_dir = (
383 unicode(os.path.abspath(task_output_dir))
384 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 self.shard_count = shard_count
386
387 self._lock = threading.Lock()
388 self._per_shard_results = {}
389 self._storage = None
390
nodire5028a92016-04-29 14:38:21 -0700391 if self.task_output_dir:
392 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700393
Vadim Shtayurab450c602014-05-12 19:23:25 -0700394 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700395 """Stores results of a single task shard, fetches output files if necessary.
396
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400397 Modifies |result| in place.
398
maruel77f720b2015-09-15 12:35:22 -0700399 shard_index is 0-based.
400
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700401 Called concurrently from multiple threads.
402 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700403 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700404 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700405 if shard_index < 0 or shard_index >= self.shard_count:
406 logging.warning(
407 'Shard index %d is outside of expected range: [0; %d]',
408 shard_index, self.shard_count - 1)
409 return
410
maruel77f720b2015-09-15 12:35:22 -0700411 if result.get('outputs_ref'):
412 ref = result['outputs_ref']
413 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
414 ref['isolatedserver'],
415 urllib.urlencode(
416 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400417
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700418 # Store result dict of that shard, ignore results we've already seen.
419 with self._lock:
420 if shard_index in self._per_shard_results:
421 logging.warning('Ignoring duplicate shard index %d', shard_index)
422 return
423 self._per_shard_results[shard_index] = result
424
425 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700426 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400427 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700428 result['outputs_ref']['isolatedserver'],
429 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400430 if storage:
431 # Output files are supposed to be small and they are not reused across
432 # tasks. So use MemoryCache for them instead of on-disk cache. Make
433 # files writable, so that calling script can delete them.
434 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700435 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400436 storage,
437 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -0700438 os.path.join(self.task_output_dir, str(shard_index)))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439
440 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700441 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442 with self._lock:
443 # Write an array of shard results with None for missing shards.
444 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 'shards': [
446 self._per_shard_results.get(i) for i in xrange(self.shard_count)
447 ],
448 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700449 # Write summary.json to task_output_dir as well.
450 if self.task_output_dir:
451 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700452 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700453 summary,
454 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700455 if self._storage:
456 self._storage.close()
457 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700458 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700459
460 def _get_storage(self, isolate_server, namespace):
461 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700462 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700463 with self._lock:
464 if not self._storage:
465 self._storage = isolateserver.get_storage(isolate_server, namespace)
466 else:
467 # Shards must all use exact same isolate server and namespace.
468 if self._storage.location != isolate_server:
469 logging.error(
470 'Task shards are using multiple isolate servers: %s and %s',
471 self._storage.location, isolate_server)
472 return None
473 if self._storage.namespace != namespace:
474 logging.error(
475 'Task shards are using multiple namespaces: %s and %s',
476 self._storage.namespace, namespace)
477 return None
478 return self._storage
479
480
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500481def now():
482 """Exists so it can be mocked easily."""
483 return time.time()
484
485
maruel77f720b2015-09-15 12:35:22 -0700486def parse_time(value):
487 """Converts serialized time from the API to datetime.datetime."""
488 # When microseconds are 0, the '.123456' suffix is elided. This means the
489 # serialized format is not consistent, which confuses the hell out of python.
490 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
491 try:
492 return datetime.datetime.strptime(value, fmt)
493 except ValueError:
494 pass
495 raise ValueError('Failed to parse %s' % value)
496
497
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700498def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700499 base_url, shard_index, task_id, timeout, should_stop, output_collector,
500 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400501 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700502
Vadim Shtayurab450c602014-05-12 19:23:25 -0700503 Returns:
504 <result dict> on success.
505 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700506 """
maruel71c61c82016-02-22 06:52:05 -0800507 assert timeout is None or isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700508 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700509 if include_perf:
510 result_url += '?include_performance_stats=true'
maruel77f720b2015-09-15 12:35:22 -0700511 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700512 started = now()
513 deadline = started + timeout if timeout else None
514 attempt = 0
515
516 while not should_stop.is_set():
517 attempt += 1
518
519 # Waiting for too long -> give up.
520 current_time = now()
521 if deadline and current_time >= deadline:
522 logging.error('retrieve_results(%s) timed out on attempt %d',
523 base_url, attempt)
524 return None
525
526 # Do not spin too fast. Spin faster at the beginning though.
527 # Start with 1 sec delay and for each 30 sec of waiting add another second
528 # of delay, until hitting 15 sec ceiling.
529 if attempt > 1:
530 max_delay = min(15, 1 + (current_time - started) / 30.0)
531 delay = min(max_delay, deadline - current_time) if deadline else max_delay
532 if delay > 0:
533 logging.debug('Waiting %.1f sec before retrying', delay)
534 should_stop.wait(delay)
535 if should_stop.is_set():
536 return None
537
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400538 # Disable internal retries in net.url_read_json, since we are doing retries
539 # ourselves.
540 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700541 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
542 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400543 result = net.url_read_json(result_url, retry_50x=False)
544 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400545 continue
maruel77f720b2015-09-15 12:35:22 -0700546
maruelbf53e042015-12-01 15:00:51 -0800547 if result.get('error'):
548 # An error occurred.
549 if result['error'].get('errors'):
550 for err in result['error']['errors']:
551 logging.warning(
552 'Error while reading task: %s; %s',
553 err.get('message'), err.get('debugInfo'))
554 elif result['error'].get('message'):
555 logging.warning(
556 'Error while reading task: %s', result['error']['message'])
557 continue
558
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400559 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700560 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400561 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700562 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700563 # Record the result, try to fetch attached output files (if any).
564 if output_collector:
565 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700566 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700567 if result.get('internal_failure'):
568 logging.error('Internal error!')
569 elif result['state'] == 'BOT_DIED':
570 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700571 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000572
573
maruel77f720b2015-09-15 12:35:22 -0700574def convert_to_old_format(result):
575 """Converts the task result data from Endpoints API format to old API format
576 for compatibility.
577
578 This goes into the file generated as --task-summary-json.
579 """
580 # Sets default.
581 result.setdefault('abandoned_ts', None)
582 result.setdefault('bot_id', None)
583 result.setdefault('bot_version', None)
584 result.setdefault('children_task_ids', [])
585 result.setdefault('completed_ts', None)
586 result.setdefault('cost_saved_usd', None)
587 result.setdefault('costs_usd', None)
588 result.setdefault('deduped_from', None)
589 result.setdefault('name', None)
590 result.setdefault('outputs_ref', None)
591 result.setdefault('properties_hash', None)
592 result.setdefault('server_versions', None)
593 result.setdefault('started_ts', None)
594 result.setdefault('tags', None)
595 result.setdefault('user', None)
596
597 # Convertion back to old API.
598 duration = result.pop('duration', None)
599 result['durations'] = [duration] if duration else []
600 exit_code = result.pop('exit_code', None)
601 result['exit_codes'] = [int(exit_code)] if exit_code else []
602 result['id'] = result.pop('task_id')
603 result['isolated_out'] = result.get('outputs_ref', None)
604 output = result.pop('output', None)
605 result['outputs'] = [output] if output else []
606 # properties_hash
607 # server_version
608 # Endpoints result 'state' as string. For compatibility with old code, convert
609 # to int.
610 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700611 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700612 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700613 if 'bot_dimensions' in result:
614 result['bot_dimensions'] = {
615 i['key']: i['value'] for i in result['bot_dimensions']
616 }
617 else:
618 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700619
620
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700621def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400622 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700623 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500624 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700626 Duplicate shards are ignored. Shards are yielded in order of completion.
627 Timed out shards are NOT yielded at all. Caller can compare number of yielded
628 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000629
630 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500631 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 +0000632 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500633
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700634 output_collector is an optional instance of TaskOutputCollector that will be
635 used to fetch files produced by a task from isolate server to the local disk.
636
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500637 Yields:
638 (index, result). In particular, 'result' is defined as the
639 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000641 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400642 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700644 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700645
maruel@chromium.org0437a732013-08-27 16:05:52 +0000646 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
647 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700648 # Adds a task to the thread pool to call 'retrieve_results' and return
649 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400650 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700651 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000652 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400653 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700654 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700655
656 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400657 for shard_index, task_id in enumerate(task_ids):
658 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700659
660 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400661 shards_remaining = range(len(task_ids))
662 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700664 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700665 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700666 shard_index, result = results_channel.pull(
667 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700668 except threading_utils.TaskChannel.Timeout:
669 if print_status_updates:
670 print(
671 'Waiting for results from the following shards: %s' %
672 ', '.join(map(str, shards_remaining)))
673 sys.stdout.flush()
674 continue
675 except Exception:
676 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700677
678 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700679 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000680 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500681 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000682 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700683
Vadim Shtayurab450c602014-05-12 19:23:25 -0700684 # Yield back results to the caller.
685 assert shard_index in shards_remaining
686 shards_remaining.remove(shard_index)
687 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700688
maruel@chromium.org0437a732013-08-27 16:05:52 +0000689 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700690 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000691 should_stop.set()
692
693
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400694def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000695 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700696 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400697 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700698 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
699 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400700 else:
701 pending = 'N/A'
702
maruel77f720b2015-09-15 12:35:22 -0700703 if metadata.get('duration') is not None:
704 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400705 else:
706 duration = 'N/A'
707
maruel77f720b2015-09-15 12:35:22 -0700708 if metadata.get('exit_code') is not None:
709 # Integers are encoded as string to not loose precision.
710 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400711 else:
712 exit_code = 'N/A'
713
714 bot_id = metadata.get('bot_id') or 'N/A'
715
maruel77f720b2015-09-15 12:35:22 -0700716 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400717 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400718 tag_footer = (
719 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
720 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400721
722 tag_len = max(len(tag_header), len(tag_footer))
723 dash_pad = '+-%s-+\n' % ('-' * tag_len)
724 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
725 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
726
727 header = dash_pad + tag_header + dash_pad
728 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700729 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400730 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000731
732
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700733def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700734 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700735 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700736 """Retrieves results of a Swarming task.
737
738 Returns:
739 process exit code that should be returned to the user.
740 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700741 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700742 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700743
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700744 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700745 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400746 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700747 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400748 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400749 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700750 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700751 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700752
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400753 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700754 shard_exit_code = metadata.get('exit_code')
755 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700756 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700757 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700758 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400759 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700760 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700761
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700762 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400763 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400764 if len(seen_shards) < len(task_ids):
765 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700766 else:
maruel77f720b2015-09-15 12:35:22 -0700767 print('%s: %s %s' % (
768 metadata.get('bot_id', 'N/A'),
769 metadata['task_id'],
770 shard_exit_code))
771 if metadata['output']:
772 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400773 if output:
774 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700775 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700776 summary = output_collector.finalize()
777 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700778 # TODO(maruel): Make this optional.
779 for i in summary['shards']:
780 if i:
781 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700782 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700783
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400784 if decorate and total_duration:
785 print('Total duration: %.1fs' % total_duration)
786
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400787 if len(seen_shards) != len(task_ids):
788 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700789 print >> sys.stderr, ('Results from some shards are missing: %s' %
790 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700791 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700792
maruela5490782015-09-30 10:56:59 -0700793 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000794
795
maruel77f720b2015-09-15 12:35:22 -0700796### API management.
797
798
799class APIError(Exception):
800 pass
801
802
803def endpoints_api_discovery_apis(host):
804 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
805 the APIs exposed by a host.
806
807 https://developers.google.com/discovery/v1/reference/apis/list
808 """
809 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
810 if data is None:
811 raise APIError('Failed to discover APIs on %s' % host)
812 out = {}
813 for api in data['items']:
814 if api['id'] == 'discovery:v1':
815 continue
816 # URL is of the following form:
817 # url = host + (
818 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
819 api_data = net.url_read_json(api['discoveryRestUrl'])
820 if api_data is None:
821 raise APIError('Failed to discover %s on %s' % (api['id'], host))
822 out[api['id']] = api_data
823 return out
824
825
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500826### Commands.
827
828
829def abort_task(_swarming, _manifest):
830 """Given a task manifest that was triggered, aborts its execution."""
831 # TODO(vadimsh): No supported by the server yet.
832
833
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400834def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400835 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500836 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500837 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500838 dest='dimensions', metavar='FOO bar',
839 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500840 parser.add_option_group(parser.filter_group)
841
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400842
Vadim Shtayurab450c602014-05-12 19:23:25 -0700843def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400844 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700845 parser.sharding_group.add_option(
846 '--shards', type='int', default=1,
847 help='Number of shards to trigger and collect.')
848 parser.add_option_group(parser.sharding_group)
849
850
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400851def add_trigger_options(parser):
852 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500853 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400854 add_filter_options(parser)
855
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400856 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500857 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500858 '-s', '--isolated',
859 help='Hash of the .isolated to grab from the isolate server')
860 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500861 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700862 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500863 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500864 '--priority', type='int', default=100,
865 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500866 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500867 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400868 help='Display name of the task. Defaults to '
869 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
870 'isolated file is provided, if a hash is provided, it defaults to '
871 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400872 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400873 '--tags', action='append', default=[],
874 help='Tags to assign to the task.')
875 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500876 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400877 help='User associated with the task. Defaults to authenticated user on '
878 'the server.')
879 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400880 '--idempotent', action='store_true', default=False,
881 help='When set, the server will actively try to find a previous task '
882 'with the same parameter and return this result instead if possible')
883 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400884 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400885 help='Seconds to allow the task to be pending for a bot to run before '
886 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400887 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400888 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400889 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400890 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400891 '--hard-timeout', type='int', default=60*60,
892 help='Seconds to allow the task to complete.')
893 parser.task_group.add_option(
894 '--io-timeout', type='int', default=20*60,
895 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500896 parser.task_group.add_option(
897 '--raw-cmd', action='store_true', default=False,
898 help='When set, the command after -- is used as-is without run_isolated. '
899 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500900 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000901
902
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500903def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500904 """Processes trigger options and uploads files to isolate server if necessary.
905 """
906 options.dimensions = dict(options.dimensions)
907 options.env = dict(options.env)
908
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500909 if not options.dimensions:
910 parser.error('Please at least specify one --dimension')
911 if options.raw_cmd:
912 if not args:
913 parser.error(
914 'Arguments with --raw-cmd should be passed after -- as command '
915 'delimiter.')
916 if options.isolate_server:
917 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
918
919 command = args
920 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500921 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500922 options.user,
923 '_'.join(
924 '%s=%s' % (k, v)
925 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700926 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500927 else:
nodir55be77b2016-05-03 09:39:57 -0700928 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500929 try:
maruel77f720b2015-09-15 12:35:22 -0700930 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500931 except ValueError as e:
932 parser.error(str(e))
933
nodir152cba62016-05-12 16:08:56 -0700934 # If inputs_ref.isolated is used, command is actually extra_args.
935 # Otherwise it's an actual command to run.
936 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -0700937 properties = TaskProperties(
nodir152cba62016-05-12 16:08:56 -0700938 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500939 dimensions=options.dimensions,
940 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700941 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -0700942 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -0700943 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500944 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700945 inputs_ref=inputs_ref,
946 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700947 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
948 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700949 return NewTaskRequest(
950 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500951 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700952 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500953 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700954 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500955 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700956 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000957
958
959def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500960 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -0800961 '-t', '--timeout', type='float',
962 help='Timeout to wait for result, set to 0 for no timeout; default to no '
963 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500964 parser.group_logging.add_option(
965 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700966 parser.group_logging.add_option(
967 '--print-status-updates', action='store_true',
968 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400969 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700970 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700971 '--task-summary-json',
972 metavar='FILE',
973 help='Dump a summary of task results to this file as json. It contains '
974 'only shards statuses as know to server directly. Any output files '
975 'emitted by the task can be collected by using --task-output-dir')
976 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700977 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700978 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700979 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700980 'directory contains per-shard directory with output files produced '
981 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -0700982 parser.task_output_group.add_option(
983 '--perf', action='store_true', default=False,
984 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700985 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000986
987
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400988@subcommand.usage('bots...')
989def CMDbot_delete(parser, args):
990 """Forcibly deletes bots from the Swarming server."""
991 parser.add_option(
992 '-f', '--force', action='store_true',
993 help='Do not prompt for confirmation')
994 options, args = parser.parse_args(args)
995 if not args:
996 parser.error('Please specific bots to delete')
997
998 bots = sorted(args)
999 if not options.force:
1000 print('Delete the following bots?')
1001 for bot in bots:
1002 print(' %s' % bot)
1003 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1004 print('Goodbye.')
1005 return 1
1006
1007 result = 0
1008 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001009 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1010 if net.url_read_json(url, data={}, method='POST') is None:
1011 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001012 result = 1
1013 return result
1014
1015
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001016def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001017 """Returns information about the bots connected to the Swarming server."""
1018 add_filter_options(parser)
1019 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001020 '--dead-only', action='store_true',
1021 help='Only print dead bots, useful to reap them and reimage broken bots')
1022 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001023 '-k', '--keep-dead', action='store_true',
1024 help='Do not filter out dead bots')
1025 parser.filter_group.add_option(
1026 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001027 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001028 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001029
1030 if options.keep_dead and options.dead_only:
1031 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001032
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001033 bots = []
1034 cursor = None
1035 limit = 250
1036 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001037 base_url = (
1038 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001039 while True:
1040 url = base_url
1041 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001042 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001043 data = net.url_read_json(url)
1044 if data is None:
1045 print >> sys.stderr, 'Failed to access %s' % options.swarming
1046 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001047 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001048 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001049 if not cursor:
1050 break
1051
maruel77f720b2015-09-15 12:35:22 -07001052 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001053 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001054 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001055 continue
maruel77f720b2015-09-15 12:35:22 -07001056 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001057 continue
1058
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001059 # If the user requested to filter on dimensions, ensure the bot has all the
1060 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001061 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001062 for key, value in options.dimensions:
1063 if key not in dimensions:
1064 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001065 # A bot can have multiple value for a key, for example,
1066 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1067 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001068 if isinstance(dimensions[key], list):
1069 if value not in dimensions[key]:
1070 break
1071 else:
1072 if value != dimensions[key]:
1073 break
1074 else:
maruel77f720b2015-09-15 12:35:22 -07001075 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001076 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001077 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001078 if bot.get('task_id'):
1079 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001080 return 0
1081
1082
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001083@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001084def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001085 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001086
1087 The result can be in multiple part if the execution was sharded. It can
1088 potentially have retries.
1089 """
1090 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001091 parser.add_option(
1092 '-j', '--json',
1093 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001094 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001095 if not args and not options.json:
1096 parser.error('Must specify at least one task id or --json.')
1097 if args and options.json:
1098 parser.error('Only use one of task id or --json.')
1099
1100 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001101 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001102 try:
maruel1ceb3872015-10-14 06:10:44 -07001103 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001104 data = json.load(f)
1105 except (IOError, ValueError):
1106 parser.error('Failed to open %s' % options.json)
1107 try:
1108 tasks = sorted(
1109 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1110 args = [t['task_id'] for t in tasks]
1111 except (KeyError, TypeError):
1112 parser.error('Failed to process %s' % options.json)
1113 if options.timeout is None:
1114 options.timeout = (
1115 data['request']['properties']['execution_timeout_secs'] +
1116 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001117 else:
1118 valid = frozenset('0123456789abcdef')
1119 if any(not valid.issuperset(task_id) for task_id in args):
1120 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001121
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001122 try:
1123 return collect(
1124 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001125 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001126 options.timeout,
1127 options.decorate,
1128 options.print_status_updates,
1129 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001130 options.task_output_dir,
1131 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001132 except Failure:
1133 on_error.report(None)
1134 return 1
1135
1136
maruelbea00862015-09-18 09:55:36 -07001137@subcommand.usage('[filename]')
1138def CMDput_bootstrap(parser, args):
1139 """Uploads a new version of bootstrap.py."""
1140 options, args = parser.parse_args(args)
1141 if len(args) != 1:
1142 parser.error('Must specify file to upload')
1143 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001144 path = unicode(os.path.abspath(args[0]))
1145 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001146 content = f.read().decode('utf-8')
1147 data = net.url_read_json(url, data={'content': content})
1148 print data
1149 return 0
1150
1151
1152@subcommand.usage('[filename]')
1153def CMDput_bot_config(parser, args):
1154 """Uploads a new version of bot_config.py."""
1155 options, args = parser.parse_args(args)
1156 if len(args) != 1:
1157 parser.error('Must specify file to upload')
1158 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001159 path = unicode(os.path.abspath(args[0]))
1160 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001161 content = f.read().decode('utf-8')
1162 data = net.url_read_json(url, data={'content': content})
1163 print data
1164 return 0
1165
1166
maruel77f720b2015-09-15 12:35:22 -07001167@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001168def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001169 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1170 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001171
1172 Examples:
maruel77f720b2015-09-15 12:35:22 -07001173 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001174 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001175
maruel77f720b2015-09-15 12:35:22 -07001176 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001177 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1178
1179 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1180 quoting is important!:
1181 swarming.py query -S server-url.com --limit 10 \\
1182 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001183 """
1184 CHUNK_SIZE = 250
1185
1186 parser.add_option(
1187 '-L', '--limit', type='int', default=200,
1188 help='Limit to enforce on limitless items (like number of tasks); '
1189 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001190 parser.add_option(
1191 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001192 parser.add_option(
1193 '--progress', action='store_true',
1194 help='Prints a dot at each request to show progress')
1195 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001196 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001197 parser.error(
1198 'Must specify only method name and optionally query args properly '
1199 'escaped.')
1200 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001201 url = base_url
1202 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001203 # Check check, change if not working out.
1204 merge_char = '&' if '?' in url else '?'
1205 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001206 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001207 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001208 # TODO(maruel): Do basic diagnostic.
1209 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001210 return 1
1211
1212 # Some items support cursors. Try to get automatically if cursors are needed
1213 # by looking at the 'cursor' items.
1214 while (
1215 data.get('cursor') and
1216 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001217 merge_char = '&' if '?' in base_url else '?'
1218 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001219 if options.limit:
1220 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001221 if options.progress:
1222 sys.stdout.write('.')
1223 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001224 new = net.url_read_json(url)
1225 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001226 if options.progress:
1227 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001228 print >> sys.stderr, 'Failed to access %s' % options.swarming
1229 return 1
maruel81b37132015-10-21 06:42:13 -07001230 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001231 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001232
maruel77f720b2015-09-15 12:35:22 -07001233 if options.progress:
1234 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001235 if options.limit and len(data.get('items', [])) > options.limit:
1236 data['items'] = data['items'][:options.limit]
1237 data.pop('cursor', None)
1238
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001239 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001240 options.json = unicode(os.path.abspath(options.json))
1241 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001242 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001243 try:
maruel77f720b2015-09-15 12:35:22 -07001244 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001245 sys.stdout.write('\n')
1246 except IOError:
1247 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001248 return 0
1249
1250
maruel77f720b2015-09-15 12:35:22 -07001251def CMDquery_list(parser, args):
1252 """Returns list of all the Swarming APIs that can be used with command
1253 'query'.
1254 """
1255 parser.add_option(
1256 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1257 options, args = parser.parse_args(args)
1258 if args:
1259 parser.error('No argument allowed.')
1260
1261 try:
1262 apis = endpoints_api_discovery_apis(options.swarming)
1263 except APIError as e:
1264 parser.error(str(e))
1265 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001266 options.json = unicode(os.path.abspath(options.json))
1267 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001268 json.dump(apis, f)
1269 else:
1270 help_url = (
1271 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1272 options.swarming)
1273 for api_id, api in sorted(apis.iteritems()):
1274 print api_id
1275 print ' ' + api['description']
1276 for resource_name, resource in sorted(api['resources'].iteritems()):
1277 print ''
1278 for method_name, method in sorted(resource['methods'].iteritems()):
1279 # Only list the GET ones.
1280 if method['httpMethod'] != 'GET':
1281 continue
1282 print '- %s.%s: %s' % (
1283 resource_name, method_name, method['path'])
1284 print ' ' + method['description']
1285 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1286 return 0
1287
1288
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001289@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001290def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001291 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001292
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001293 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001294 """
1295 add_trigger_options(parser)
1296 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001297 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001298 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001299 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001300 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001301 tasks = trigger_task_shards(
1302 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001303 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001304 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001305 'Failed to trigger %s(%s): %s' %
1306 (options.task_name, args[0], e.args[0]))
1307 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001308 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001309 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001310 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001311 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001312 task_ids = [
1313 t['task_id']
1314 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1315 ]
maruel71c61c82016-02-22 06:52:05 -08001316 if options.timeout is None:
1317 options.timeout = (
1318 task_request.properties.execution_timeout_secs +
1319 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001320 try:
1321 return collect(
1322 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001323 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001324 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001325 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001326 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001327 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001328 options.task_output_dir,
1329 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001330 except Failure:
1331 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001332 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001333
1334
maruel18122c62015-10-23 06:31:23 -07001335@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001336def CMDreproduce(parser, args):
1337 """Runs a task locally that was triggered on the server.
1338
1339 This running locally the same commands that have been run on the bot. The data
1340 downloaded will be in a subdirectory named 'work' of the current working
1341 directory.
maruel18122c62015-10-23 06:31:23 -07001342
1343 You can pass further additional arguments to the target command by passing
1344 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001345 """
maruelc070e672016-02-22 17:32:57 -08001346 parser.add_option(
1347 '--output-dir', metavar='DIR', default='',
1348 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001349 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001350 extra_args = []
1351 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001352 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001353 if len(args) > 1:
1354 if args[1] == '--':
1355 if len(args) > 2:
1356 extra_args = args[2:]
1357 else:
1358 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001359
maruel77f720b2015-09-15 12:35:22 -07001360 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001361 request = net.url_read_json(url)
1362 if not request:
1363 print >> sys.stderr, 'Failed to retrieve request data for the task'
1364 return 1
1365
maruel12e30012015-10-09 11:55:35 -07001366 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001367 if fs.isdir(workdir):
1368 parser.error('Please delete the directory \'work\' first')
1369 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001370
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001371 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001372 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001373 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001374 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001375 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001376 for i in properties['env']:
1377 key = i['key'].encode('utf-8')
1378 if not i['value']:
1379 env.pop(key, None)
1380 else:
1381 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001382
nodir152cba62016-05-12 16:08:56 -07001383 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001384 # Create the tree.
1385 with isolateserver.get_storage(
1386 properties['inputs_ref']['isolatedserver'],
1387 properties['inputs_ref']['namespace']) as storage:
1388 bundle = isolateserver.fetch_isolated(
1389 properties['inputs_ref']['isolated'],
1390 storage,
1391 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -07001392 workdir)
maruel29ab2fd2015-10-16 11:44:01 -07001393 command = bundle.command
1394 if bundle.relative_cwd:
1395 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001396 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001397 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1398 new_command = run_isolated.process_command(command, options.output_dir)
1399 if not options.output_dir and new_command != command:
1400 parser.error('The task has outputs, you must use --output-dir')
1401 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001402 else:
1403 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001404 try:
maruel18122c62015-10-23 06:31:23 -07001405 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001406 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001407 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001408 print >> sys.stderr, str(e)
1409 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001410
1411
maruel0eb1d1b2015-10-02 14:48:21 -07001412@subcommand.usage('bot_id')
1413def CMDterminate(parser, args):
1414 """Tells a bot to gracefully shut itself down as soon as it can.
1415
1416 This is done by completing whatever current task there is then exiting the bot
1417 process.
1418 """
1419 parser.add_option(
1420 '--wait', action='store_true', help='Wait for the bot to terminate')
1421 options, args = parser.parse_args(args)
1422 if len(args) != 1:
1423 parser.error('Please provide the bot id')
1424 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1425 request = net.url_read_json(url, data={})
1426 if not request:
1427 print >> sys.stderr, 'Failed to ask for termination'
1428 return 1
1429 if options.wait:
1430 return collect(
maruel9531ce02016-04-13 06:11:23 -07001431 options.swarming, [request['task_id']], 0., False, False, None, None,
1432 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001433 return 0
1434
1435
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001436@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001437def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001438 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001439
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001440 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001441 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001442
1443 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001444
1445 Passes all extra arguments provided after '--' as additional command line
1446 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001447 """
1448 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001449 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001450 parser.add_option(
1451 '--dump-json',
1452 metavar='FILE',
1453 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001454 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001455 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001456 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001457 tasks = trigger_task_shards(
1458 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001459 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001460 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001461 tasks_sorted = sorted(
1462 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001463 if options.dump_json:
1464 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001465 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001466 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001467 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001468 }
maruel46b015f2015-10-13 18:40:35 -07001469 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001470 print('To collect results, use:')
1471 print(' swarming.py collect -S %s --json %s' %
1472 (options.swarming, options.dump_json))
1473 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001474 print('To collect results, use:')
1475 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001476 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1477 print('Or visit:')
1478 for t in tasks_sorted:
1479 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001480 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001481 except Failure:
1482 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001483 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001484
1485
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001486class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001487 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001488 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001489 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001490 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001491 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001492 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001493 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001494 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001495 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001496 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001497
1498 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001499 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001500 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001501 auth.process_auth_options(self, options)
1502 user = self._process_swarming(options)
1503 if hasattr(options, 'user') and not options.user:
1504 options.user = user
1505 return options, args
1506
1507 def _process_swarming(self, options):
1508 """Processes the --swarming option and aborts if not specified.
1509
1510 Returns the identity as determined by the server.
1511 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001512 if not options.swarming:
1513 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001514 try:
1515 options.swarming = net.fix_url(options.swarming)
1516 except ValueError as e:
1517 self.error('--swarming %s' % e)
1518 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001519 try:
1520 user = auth.ensure_logged_in(options.swarming)
1521 except ValueError as e:
1522 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001523 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001524
1525
1526def main(args):
1527 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001528 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001529
1530
1531if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001532 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001533 fix_encoding.fix_encoding()
1534 tools.disable_buffering()
1535 colorama.init()
1536 sys.exit(main(sys.argv[1:]))