blob: bf590e3a044540f99715699ec1d1628861f77ee5 [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'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700615 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700616 }
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:
maruelfd0a90c2016-06-10 11:51:10 -0700996 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400997
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
maruelfd0a90c2016-06-10 11:51:10 -07001083@subcommand.usage('task_id')
1084def CMDcancel(parser, args):
1085 """Cancels a task."""
1086 options, args = parser.parse_args(args)
1087 if not args:
1088 parser.error('Please specify the task to cancel')
1089 for task_id in args:
1090 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
1091 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1092 print('Deleting %s failed. Probably already gone' % task_id)
1093 return 1
1094 return 0
1095
1096
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001097@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001098def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001099 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001100
1101 The result can be in multiple part if the execution was sharded. It can
1102 potentially have retries.
1103 """
1104 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001105 parser.add_option(
1106 '-j', '--json',
1107 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001108 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001109 if not args and not options.json:
1110 parser.error('Must specify at least one task id or --json.')
1111 if args and options.json:
1112 parser.error('Only use one of task id or --json.')
1113
1114 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001115 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001116 try:
maruel1ceb3872015-10-14 06:10:44 -07001117 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001118 data = json.load(f)
1119 except (IOError, ValueError):
1120 parser.error('Failed to open %s' % options.json)
1121 try:
1122 tasks = sorted(
1123 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1124 args = [t['task_id'] for t in tasks]
1125 except (KeyError, TypeError):
1126 parser.error('Failed to process %s' % options.json)
1127 if options.timeout is None:
1128 options.timeout = (
1129 data['request']['properties']['execution_timeout_secs'] +
1130 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001131 else:
1132 valid = frozenset('0123456789abcdef')
1133 if any(not valid.issuperset(task_id) for task_id in args):
1134 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001135
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001136 try:
1137 return collect(
1138 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001139 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001140 options.timeout,
1141 options.decorate,
1142 options.print_status_updates,
1143 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001144 options.task_output_dir,
1145 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001146 except Failure:
1147 on_error.report(None)
1148 return 1
1149
1150
maruelbea00862015-09-18 09:55:36 -07001151@subcommand.usage('[filename]')
1152def CMDput_bootstrap(parser, args):
1153 """Uploads a new version of bootstrap.py."""
1154 options, args = parser.parse_args(args)
1155 if len(args) != 1:
1156 parser.error('Must specify file to upload')
1157 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001158 path = unicode(os.path.abspath(args[0]))
1159 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001160 content = f.read().decode('utf-8')
1161 data = net.url_read_json(url, data={'content': content})
1162 print data
1163 return 0
1164
1165
1166@subcommand.usage('[filename]')
1167def CMDput_bot_config(parser, args):
1168 """Uploads a new version of bot_config.py."""
1169 options, args = parser.parse_args(args)
1170 if len(args) != 1:
1171 parser.error('Must specify file to upload')
1172 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001173 path = unicode(os.path.abspath(args[0]))
1174 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001175 content = f.read().decode('utf-8')
1176 data = net.url_read_json(url, data={'content': content})
1177 print data
1178 return 0
1179
1180
maruel77f720b2015-09-15 12:35:22 -07001181@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001182def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001183 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1184 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001185
1186 Examples:
maruel77f720b2015-09-15 12:35:22 -07001187 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001188 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001189
maruel77f720b2015-09-15 12:35:22 -07001190 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001191 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1192
1193 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1194 quoting is important!:
1195 swarming.py query -S server-url.com --limit 10 \\
1196 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001197 """
1198 CHUNK_SIZE = 250
1199
1200 parser.add_option(
1201 '-L', '--limit', type='int', default=200,
1202 help='Limit to enforce on limitless items (like number of tasks); '
1203 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001204 parser.add_option(
1205 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001206 parser.add_option(
1207 '--progress', action='store_true',
1208 help='Prints a dot at each request to show progress')
1209 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001210 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001211 parser.error(
1212 'Must specify only method name and optionally query args properly '
1213 'escaped.')
1214 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001215 url = base_url
1216 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001217 # Check check, change if not working out.
1218 merge_char = '&' if '?' in url else '?'
1219 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001220 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001221 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001222 # TODO(maruel): Do basic diagnostic.
1223 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001224 return 1
1225
1226 # Some items support cursors. Try to get automatically if cursors are needed
1227 # by looking at the 'cursor' items.
1228 while (
1229 data.get('cursor') and
1230 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001231 merge_char = '&' if '?' in base_url else '?'
1232 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001233 if options.limit:
1234 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001235 if options.progress:
1236 sys.stdout.write('.')
1237 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001238 new = net.url_read_json(url)
1239 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001240 if options.progress:
1241 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001242 print >> sys.stderr, 'Failed to access %s' % options.swarming
1243 return 1
maruel81b37132015-10-21 06:42:13 -07001244 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001245 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001246
maruel77f720b2015-09-15 12:35:22 -07001247 if options.progress:
1248 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001249 if options.limit and len(data.get('items', [])) > options.limit:
1250 data['items'] = data['items'][:options.limit]
1251 data.pop('cursor', None)
1252
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001253 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001254 options.json = unicode(os.path.abspath(options.json))
1255 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001256 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001257 try:
maruel77f720b2015-09-15 12:35:22 -07001258 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001259 sys.stdout.write('\n')
1260 except IOError:
1261 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001262 return 0
1263
1264
maruel77f720b2015-09-15 12:35:22 -07001265def CMDquery_list(parser, args):
1266 """Returns list of all the Swarming APIs that can be used with command
1267 'query'.
1268 """
1269 parser.add_option(
1270 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1271 options, args = parser.parse_args(args)
1272 if args:
1273 parser.error('No argument allowed.')
1274
1275 try:
1276 apis = endpoints_api_discovery_apis(options.swarming)
1277 except APIError as e:
1278 parser.error(str(e))
1279 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001280 options.json = unicode(os.path.abspath(options.json))
1281 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001282 json.dump(apis, f)
1283 else:
1284 help_url = (
1285 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1286 options.swarming)
1287 for api_id, api in sorted(apis.iteritems()):
1288 print api_id
1289 print ' ' + api['description']
1290 for resource_name, resource in sorted(api['resources'].iteritems()):
1291 print ''
1292 for method_name, method in sorted(resource['methods'].iteritems()):
1293 # Only list the GET ones.
1294 if method['httpMethod'] != 'GET':
1295 continue
1296 print '- %s.%s: %s' % (
1297 resource_name, method_name, method['path'])
1298 print ' ' + method['description']
1299 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1300 return 0
1301
1302
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001303@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001304def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001305 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001306
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001307 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001308 """
1309 add_trigger_options(parser)
1310 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001311 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001312 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001313 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001314 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001315 tasks = trigger_task_shards(
1316 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001317 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001318 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001319 'Failed to trigger %s(%s): %s' %
1320 (options.task_name, args[0], e.args[0]))
1321 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001322 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001323 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001324 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001325 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001326 task_ids = [
1327 t['task_id']
1328 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1329 ]
maruel71c61c82016-02-22 06:52:05 -08001330 if options.timeout is None:
1331 options.timeout = (
1332 task_request.properties.execution_timeout_secs +
1333 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001334 try:
1335 return collect(
1336 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001337 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001338 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001339 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001340 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001341 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001342 options.task_output_dir,
1343 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001344 except Failure:
1345 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001346 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001347
1348
maruel18122c62015-10-23 06:31:23 -07001349@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001350def CMDreproduce(parser, args):
1351 """Runs a task locally that was triggered on the server.
1352
1353 This running locally the same commands that have been run on the bot. The data
1354 downloaded will be in a subdirectory named 'work' of the current working
1355 directory.
maruel18122c62015-10-23 06:31:23 -07001356
1357 You can pass further additional arguments to the target command by passing
1358 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001359 """
maruelc070e672016-02-22 17:32:57 -08001360 parser.add_option(
1361 '--output-dir', metavar='DIR', default='',
1362 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001363 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001364 extra_args = []
1365 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001366 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001367 if len(args) > 1:
1368 if args[1] == '--':
1369 if len(args) > 2:
1370 extra_args = args[2:]
1371 else:
1372 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001373
maruel77f720b2015-09-15 12:35:22 -07001374 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001375 request = net.url_read_json(url)
1376 if not request:
1377 print >> sys.stderr, 'Failed to retrieve request data for the task'
1378 return 1
1379
maruel12e30012015-10-09 11:55:35 -07001380 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001381 if fs.isdir(workdir):
1382 parser.error('Please delete the directory \'work\' first')
1383 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001384
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001385 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001386 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001387 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001388 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001389 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001390 for i in properties['env']:
1391 key = i['key'].encode('utf-8')
1392 if not i['value']:
1393 env.pop(key, None)
1394 else:
1395 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001396
nodir152cba62016-05-12 16:08:56 -07001397 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001398 # Create the tree.
1399 with isolateserver.get_storage(
1400 properties['inputs_ref']['isolatedserver'],
1401 properties['inputs_ref']['namespace']) as storage:
1402 bundle = isolateserver.fetch_isolated(
1403 properties['inputs_ref']['isolated'],
1404 storage,
1405 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -07001406 workdir)
maruel29ab2fd2015-10-16 11:44:01 -07001407 command = bundle.command
1408 if bundle.relative_cwd:
1409 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001410 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001411 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001412 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001413 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001414 if not options.output_dir and new_command != command:
1415 parser.error('The task has outputs, you must use --output-dir')
1416 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001417 else:
1418 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001419 try:
maruel18122c62015-10-23 06:31:23 -07001420 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001421 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001422 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001423 print >> sys.stderr, str(e)
1424 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001425
1426
maruel0eb1d1b2015-10-02 14:48:21 -07001427@subcommand.usage('bot_id')
1428def CMDterminate(parser, args):
1429 """Tells a bot to gracefully shut itself down as soon as it can.
1430
1431 This is done by completing whatever current task there is then exiting the bot
1432 process.
1433 """
1434 parser.add_option(
1435 '--wait', action='store_true', help='Wait for the bot to terminate')
1436 options, args = parser.parse_args(args)
1437 if len(args) != 1:
1438 parser.error('Please provide the bot id')
1439 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1440 request = net.url_read_json(url, data={})
1441 if not request:
1442 print >> sys.stderr, 'Failed to ask for termination'
1443 return 1
1444 if options.wait:
1445 return collect(
maruel9531ce02016-04-13 06:11:23 -07001446 options.swarming, [request['task_id']], 0., False, False, None, None,
1447 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001448 return 0
1449
1450
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001451@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001452def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001453 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001454
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001455 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001456 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001457
1458 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001459
1460 Passes all extra arguments provided after '--' as additional command line
1461 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001462 """
1463 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001464 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001465 parser.add_option(
1466 '--dump-json',
1467 metavar='FILE',
1468 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001469 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001470 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001471 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001472 tasks = trigger_task_shards(
1473 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001474 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001475 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001476 tasks_sorted = sorted(
1477 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001478 if options.dump_json:
1479 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001480 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001481 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001482 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001483 }
maruel46b015f2015-10-13 18:40:35 -07001484 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001485 print('To collect results, use:')
1486 print(' swarming.py collect -S %s --json %s' %
1487 (options.swarming, options.dump_json))
1488 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001489 print('To collect results, use:')
1490 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001491 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1492 print('Or visit:')
1493 for t in tasks_sorted:
1494 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001495 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001496 except Failure:
1497 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001498 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001499
1500
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001501class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001502 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001503 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001504 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001505 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001506 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001507 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001508 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001509 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001510 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001511 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001512
1513 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001514 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001515 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001516 auth.process_auth_options(self, options)
1517 user = self._process_swarming(options)
1518 if hasattr(options, 'user') and not options.user:
1519 options.user = user
1520 return options, args
1521
1522 def _process_swarming(self, options):
1523 """Processes the --swarming option and aborts if not specified.
1524
1525 Returns the identity as determined by the server.
1526 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001527 if not options.swarming:
1528 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001529 try:
1530 options.swarming = net.fix_url(options.swarming)
1531 except ValueError as e:
1532 self.error('--swarming %s' % e)
1533 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001534 try:
1535 user = auth.ensure_logged_in(options.swarming)
1536 except ValueError as e:
1537 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001538 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001539
1540
1541def main(args):
1542 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001543 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001544
1545
1546if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001547 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001548 fix_encoding.fix_encoding()
1549 tools.disable_buffering()
1550 colorama.init()
1551 sys.exit(main(sys.argv[1:]))