blob: f71e058a62448eafb40d0a86ef1209bbee2aaed7 [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
borenet02f772b2016-06-22 12:42:19 -07008__version__ = '0.8.6'
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
tansella4949442016-06-23 22:34:32 -070043ROOT_DIR = os.path.dirname(os.path.abspath(
44 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050045
46
47class Failure(Exception):
48 """Generic failure."""
49 pass
50
51
52### Isolated file handling.
53
54
maruel77f720b2015-09-15 12:35:22 -070055def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050056 """Archives a .isolated file if needed.
57
58 Returns the file hash to trigger and a bool specifying if it was a file (True)
59 or a hash (False).
60 """
61 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070062 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070063 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050064 if not file_hash:
65 on_error.report('Archival failure %s' % arg)
66 return None, True
67 return file_hash, True
68 elif isolated_format.is_valid_hash(arg, algo):
69 return arg, False
70 else:
71 on_error.report('Invalid hash %s' % arg)
72 return None, False
73
74
75def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050076 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050077
78 Returns:
maruel77f720b2015-09-15 12:35:22 -070079 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050080 """
81 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070082 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050083 if not options.isolated:
84 if '--' in args:
85 index = args.index('--')
86 isolated_cmd_args = args[index+1:]
87 args = args[:index]
88 else:
89 # optparse eats '--' sometimes.
90 isolated_cmd_args = args[1:]
91 args = args[:1]
92 if len(args) != 1:
93 raise ValueError(
94 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
95 'process.')
96 # Old code. To be removed eventually.
97 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070098 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050099 if not options.isolated:
100 raise ValueError('Invalid argument %s' % args[0])
101 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500102 if '--' in args:
103 index = args.index('--')
104 isolated_cmd_args = args[index+1:]
105 if index != 0:
106 raise ValueError('Unexpected arguments.')
107 else:
108 # optparse eats '--' sometimes.
109 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500111 # If a file name was passed, use its base name of the isolated hash.
112 # Otherwise, use user name as an approximation of a task name.
113 if not options.task_name:
114 if is_file:
115 key = os.path.splitext(os.path.basename(args[0]))[0]
116 else:
117 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500118 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500119 key,
120 '_'.join(
121 '%s=%s' % (k, v)
122 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500123 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500124
maruel77f720b2015-09-15 12:35:22 -0700125 inputs_ref = FilesRef(
nodir152cba62016-05-12 16:08:56 -0700126 isolated=options.isolated,
127 isolatedserver=options.isolate_server,
128 namespace=options.namespace)
maruel77f720b2015-09-15 12:35:22 -0700129 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500130
131
132### Triggering.
133
134
maruel77f720b2015-09-15 12:35:22 -0700135# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -0700136CipdPackage = collections.namedtuple(
137 'CipdPackage',
138 [
139 'package_name',
140 'path',
141 'version',
142 ])
143
144
145# See ../appengine/swarming/swarming_rpcs.py.
146CipdInput = collections.namedtuple(
147 'CipdInput',
148 [
149 'client_package',
150 'packages',
151 'server',
152 ])
153
154
155# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700156FilesRef = collections.namedtuple(
157 'FilesRef',
158 [
159 'isolated',
160 'isolatedserver',
161 'namespace',
162 ])
163
164
165# See ../appengine/swarming/swarming_rpcs.py.
166TaskProperties = collections.namedtuple(
167 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500168 [
borenet02f772b2016-06-22 12:42:19 -0700169 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 'dimensions',
172 'env',
maruel77f720b2015-09-15 12:35:22 -0700173 'execution_timeout_secs',
174 'extra_args',
175 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500176 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700177 'inputs_ref',
178 'io_timeout_secs',
179 ])
180
181
182# See ../appengine/swarming/swarming_rpcs.py.
183NewTaskRequest = collections.namedtuple(
184 'NewTaskRequest',
185 [
186 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500187 'name',
maruel77f720b2015-09-15 12:35:22 -0700188 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500189 'priority',
maruel77f720b2015-09-15 12:35:22 -0700190 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500191 'tags',
192 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500193 ])
194
195
maruel77f720b2015-09-15 12:35:22 -0700196def namedtuple_to_dict(value):
197 """Recursively converts a namedtuple to a dict."""
198 out = dict(value._asdict())
199 for k, v in out.iteritems():
200 if hasattr(v, '_asdict'):
201 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700202 elif isinstance(v, (list, tuple)):
203 l = []
204 for elem in v:
205 if hasattr(elem, '_asdict'):
206 l.append(namedtuple_to_dict(elem))
207 else:
208 l.append(elem)
209 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700210 return out
211
212
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500213def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800214 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700215
216 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500217 """
maruel77f720b2015-09-15 12:35:22 -0700218 out = namedtuple_to_dict(task_request)
219 # Maps are not supported until protobuf v3.
220 out['properties']['dimensions'] = [
221 {'key': k, 'value': v}
222 for k, v in out['properties']['dimensions'].iteritems()
223 ]
224 out['properties']['dimensions'].sort(key=lambda x: x['key'])
225 out['properties']['env'] = [
226 {'key': k, 'value': v}
227 for k, v in out['properties']['env'].iteritems()
228 ]
229 out['properties']['env'].sort(key=lambda x: x['key'])
230 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500231
232
maruel77f720b2015-09-15 12:35:22 -0700233def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500234 """Triggers a request on the Swarming server and returns the json data.
235
236 It's the low-level function.
237
238 Returns:
239 {
240 'request': {
241 'created_ts': u'2010-01-02 03:04:05',
242 'name': ..
243 },
244 'task_id': '12300',
245 }
246 """
247 logging.info('Triggering: %s', raw_request['name'])
248
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500249 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700250 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251 if not result:
252 on_error.report('Failed to trigger task %s' % raw_request['name'])
253 return None
maruele557bce2015-11-17 09:01:27 -0800254 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800255 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800256 msg = 'Failed to trigger task %s' % raw_request['name']
257 if result['error'].get('errors'):
258 for err in result['error']['errors']:
259 if err.get('message'):
260 msg += '\nMessage: %s' % err['message']
261 if err.get('debugInfo'):
262 msg += '\nDebug info:\n%s' % err['debugInfo']
263 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800264 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800265
266 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800267 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500268 return result
269
270
271def setup_googletest(env, shards, index):
272 """Sets googletest specific environment variables."""
273 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700274 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
275 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
276 env = env[:]
277 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
278 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500279 return env
280
281
282def trigger_task_shards(swarming, task_request, shards):
283 """Triggers one or many subtasks of a sharded task.
284
285 Returns:
286 Dict with task details, returned to caller as part of --dump-json output.
287 None in case of failure.
288 """
289 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700290 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500291 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700292 req['properties']['env'] = setup_googletest(
293 req['properties']['env'], shards, index)
294 req['name'] += ':%s:%s' % (index, shards)
295 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500296
297 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500298 tasks = {}
299 priority_warning = False
300 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700301 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500302 if not task:
303 break
304 logging.info('Request result: %s', task)
305 if (not priority_warning and
306 task['request']['priority'] != task_request.priority):
307 priority_warning = True
308 print >> sys.stderr, (
309 'Priority was reset to %s' % task['request']['priority'])
310 tasks[request['name']] = {
311 'shard_index': index,
312 'task_id': task['task_id'],
313 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
314 }
315
316 # Some shards weren't triggered. Abort everything.
317 if len(tasks) != len(requests):
318 if tasks:
319 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
320 len(tasks), len(requests))
321 for task_dict in tasks.itervalues():
322 abort_task(swarming, task_dict['task_id'])
323 return None
324
325 return tasks
326
327
328### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000329
330
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700331# How often to print status updates to stdout in 'collect'.
332STATUS_UPDATE_INTERVAL = 15 * 60.
333
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400334
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400335class State(object):
336 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000337
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400338 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
339 values are part of the API so if they change, the API changed.
340
341 It's in fact an enum. Values should be in decreasing order of importance.
342 """
343 RUNNING = 0x10
344 PENDING = 0x20
345 EXPIRED = 0x30
346 TIMED_OUT = 0x40
347 BOT_DIED = 0x50
348 CANCELED = 0x60
349 COMPLETED = 0x70
350
maruel77f720b2015-09-15 12:35:22 -0700351 STATES = (
352 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
353 'COMPLETED')
354 STATES_RUNNING = ('RUNNING', 'PENDING')
355 STATES_NOT_RUNNING = (
356 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
357 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
358 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400359
360 _NAMES = {
361 RUNNING: 'Running',
362 PENDING: 'Pending',
363 EXPIRED: 'Expired',
364 TIMED_OUT: 'Execution timed out',
365 BOT_DIED: 'Bot died',
366 CANCELED: 'User canceled',
367 COMPLETED: 'Completed',
368 }
369
maruel77f720b2015-09-15 12:35:22 -0700370 _ENUMS = {
371 'RUNNING': RUNNING,
372 'PENDING': PENDING,
373 'EXPIRED': EXPIRED,
374 'TIMED_OUT': TIMED_OUT,
375 'BOT_DIED': BOT_DIED,
376 'CANCELED': CANCELED,
377 'COMPLETED': COMPLETED,
378 }
379
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400380 @classmethod
381 def to_string(cls, state):
382 """Returns a user-readable string representing a State."""
383 if state not in cls._NAMES:
384 raise ValueError('Invalid state %s' % state)
385 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000386
maruel77f720b2015-09-15 12:35:22 -0700387 @classmethod
388 def from_enum(cls, state):
389 """Returns int value based on the string."""
390 if state not in cls._ENUMS:
391 raise ValueError('Invalid state %s' % state)
392 return cls._ENUMS[state]
393
maruel@chromium.org0437a732013-08-27 16:05:52 +0000394
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700395class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700396 """Assembles task execution summary (for --task-summary-json output).
397
398 Optionally fetches task outputs from isolate server to local disk (used when
399 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400
401 This object is shared among multiple threads running 'retrieve_results'
402 function, in particular they call 'process_shard_result' method in parallel.
403 """
404
maruel0eb1d1b2015-10-02 14:48:21 -0700405 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700406 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
407
408 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700409 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700410 shard_count: expected number of task shards.
411 """
maruel12e30012015-10-09 11:55:35 -0700412 self.task_output_dir = (
413 unicode(os.path.abspath(task_output_dir))
414 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700415 self.shard_count = shard_count
416
417 self._lock = threading.Lock()
418 self._per_shard_results = {}
419 self._storage = None
420
nodire5028a92016-04-29 14:38:21 -0700421 if self.task_output_dir:
422 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700423
Vadim Shtayurab450c602014-05-12 19:23:25 -0700424 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700425 """Stores results of a single task shard, fetches output files if necessary.
426
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400427 Modifies |result| in place.
428
maruel77f720b2015-09-15 12:35:22 -0700429 shard_index is 0-based.
430
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700431 Called concurrently from multiple threads.
432 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700434 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700435 if shard_index < 0 or shard_index >= self.shard_count:
436 logging.warning(
437 'Shard index %d is outside of expected range: [0; %d]',
438 shard_index, self.shard_count - 1)
439 return
440
maruel77f720b2015-09-15 12:35:22 -0700441 if result.get('outputs_ref'):
442 ref = result['outputs_ref']
443 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
444 ref['isolatedserver'],
445 urllib.urlencode(
446 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400447
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 # Store result dict of that shard, ignore results we've already seen.
449 with self._lock:
450 if shard_index in self._per_shard_results:
451 logging.warning('Ignoring duplicate shard index %d', shard_index)
452 return
453 self._per_shard_results[shard_index] = result
454
455 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700456 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400457 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700458 result['outputs_ref']['isolatedserver'],
459 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400460 if storage:
461 # Output files are supposed to be small and they are not reused across
462 # tasks. So use MemoryCache for them instead of on-disk cache. Make
463 # files writable, so that calling script can delete them.
464 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700465 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400466 storage,
467 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700468 os.path.join(self.task_output_dir, str(shard_index)),
469 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700470
471 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700472 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700473 with self._lock:
474 # Write an array of shard results with None for missing shards.
475 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700476 'shards': [
477 self._per_shard_results.get(i) for i in xrange(self.shard_count)
478 ],
479 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700480 # Write summary.json to task_output_dir as well.
481 if self.task_output_dir:
482 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700483 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700484 summary,
485 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700486 if self._storage:
487 self._storage.close()
488 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700489 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700490
491 def _get_storage(self, isolate_server, namespace):
492 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700493 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700494 with self._lock:
495 if not self._storage:
496 self._storage = isolateserver.get_storage(isolate_server, namespace)
497 else:
498 # Shards must all use exact same isolate server and namespace.
499 if self._storage.location != isolate_server:
500 logging.error(
501 'Task shards are using multiple isolate servers: %s and %s',
502 self._storage.location, isolate_server)
503 return None
504 if self._storage.namespace != namespace:
505 logging.error(
506 'Task shards are using multiple namespaces: %s and %s',
507 self._storage.namespace, namespace)
508 return None
509 return self._storage
510
511
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500512def now():
513 """Exists so it can be mocked easily."""
514 return time.time()
515
516
maruel77f720b2015-09-15 12:35:22 -0700517def parse_time(value):
518 """Converts serialized time from the API to datetime.datetime."""
519 # When microseconds are 0, the '.123456' suffix is elided. This means the
520 # serialized format is not consistent, which confuses the hell out of python.
521 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
522 try:
523 return datetime.datetime.strptime(value, fmt)
524 except ValueError:
525 pass
526 raise ValueError('Failed to parse %s' % value)
527
528
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700529def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700530 base_url, shard_index, task_id, timeout, should_stop, output_collector,
531 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400532 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700533
Vadim Shtayurab450c602014-05-12 19:23:25 -0700534 Returns:
535 <result dict> on success.
536 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700537 """
maruel71c61c82016-02-22 06:52:05 -0800538 assert timeout is None or isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700539 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700540 if include_perf:
541 result_url += '?include_performance_stats=true'
maruel77f720b2015-09-15 12:35:22 -0700542 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700543 started = now()
544 deadline = started + timeout if timeout else None
545 attempt = 0
546
547 while not should_stop.is_set():
548 attempt += 1
549
550 # Waiting for too long -> give up.
551 current_time = now()
552 if deadline and current_time >= deadline:
553 logging.error('retrieve_results(%s) timed out on attempt %d',
554 base_url, attempt)
555 return None
556
557 # Do not spin too fast. Spin faster at the beginning though.
558 # Start with 1 sec delay and for each 30 sec of waiting add another second
559 # of delay, until hitting 15 sec ceiling.
560 if attempt > 1:
561 max_delay = min(15, 1 + (current_time - started) / 30.0)
562 delay = min(max_delay, deadline - current_time) if deadline else max_delay
563 if delay > 0:
564 logging.debug('Waiting %.1f sec before retrying', delay)
565 should_stop.wait(delay)
566 if should_stop.is_set():
567 return None
568
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400569 # Disable internal retries in net.url_read_json, since we are doing retries
570 # ourselves.
571 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700572 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
573 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400574 result = net.url_read_json(result_url, retry_50x=False)
575 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400576 continue
maruel77f720b2015-09-15 12:35:22 -0700577
maruelbf53e042015-12-01 15:00:51 -0800578 if result.get('error'):
579 # An error occurred.
580 if result['error'].get('errors'):
581 for err in result['error']['errors']:
582 logging.warning(
583 'Error while reading task: %s; %s',
584 err.get('message'), err.get('debugInfo'))
585 elif result['error'].get('message'):
586 logging.warning(
587 'Error while reading task: %s', result['error']['message'])
588 continue
589
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400590 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700591 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400592 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700593 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700594 # Record the result, try to fetch attached output files (if any).
595 if output_collector:
596 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700597 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700598 if result.get('internal_failure'):
599 logging.error('Internal error!')
600 elif result['state'] == 'BOT_DIED':
601 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700602 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000603
604
maruel77f720b2015-09-15 12:35:22 -0700605def convert_to_old_format(result):
606 """Converts the task result data from Endpoints API format to old API format
607 for compatibility.
608
609 This goes into the file generated as --task-summary-json.
610 """
611 # Sets default.
612 result.setdefault('abandoned_ts', None)
613 result.setdefault('bot_id', None)
614 result.setdefault('bot_version', None)
615 result.setdefault('children_task_ids', [])
616 result.setdefault('completed_ts', None)
617 result.setdefault('cost_saved_usd', None)
618 result.setdefault('costs_usd', None)
619 result.setdefault('deduped_from', None)
620 result.setdefault('name', None)
621 result.setdefault('outputs_ref', None)
622 result.setdefault('properties_hash', None)
623 result.setdefault('server_versions', None)
624 result.setdefault('started_ts', None)
625 result.setdefault('tags', None)
626 result.setdefault('user', None)
627
628 # Convertion back to old API.
629 duration = result.pop('duration', None)
630 result['durations'] = [duration] if duration else []
631 exit_code = result.pop('exit_code', None)
632 result['exit_codes'] = [int(exit_code)] if exit_code else []
633 result['id'] = result.pop('task_id')
634 result['isolated_out'] = result.get('outputs_ref', None)
635 output = result.pop('output', None)
636 result['outputs'] = [output] if output else []
637 # properties_hash
638 # server_version
639 # Endpoints result 'state' as string. For compatibility with old code, convert
640 # to int.
641 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700642 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700643 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700644 if 'bot_dimensions' in result:
645 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700646 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700647 }
648 else:
649 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700650
651
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700652def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400653 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700654 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500655 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700657 Duplicate shards are ignored. Shards are yielded in order of completion.
658 Timed out shards are NOT yielded at all. Caller can compare number of yielded
659 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000660
661 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500662 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 +0000663 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500664
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700665 output_collector is an optional instance of TaskOutputCollector that will be
666 used to fetch files produced by a task from isolate server to the local disk.
667
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500668 Yields:
669 (index, result). In particular, 'result' is defined as the
670 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000671 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000672 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400673 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700674 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700675 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700676
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
678 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700679 # Adds a task to the thread pool to call 'retrieve_results' and return
680 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400681 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700682 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000683 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400684 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700685 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700686
687 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400688 for shard_index, task_id in enumerate(task_ids):
689 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700690
691 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400692 shards_remaining = range(len(task_ids))
693 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700694 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700695 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700696 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700697 shard_index, result = results_channel.pull(
698 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700699 except threading_utils.TaskChannel.Timeout:
700 if print_status_updates:
701 print(
702 'Waiting for results from the following shards: %s' %
703 ', '.join(map(str, shards_remaining)))
704 sys.stdout.flush()
705 continue
706 except Exception:
707 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700708
709 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700710 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000711 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500712 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000713 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700714
Vadim Shtayurab450c602014-05-12 19:23:25 -0700715 # Yield back results to the caller.
716 assert shard_index in shards_remaining
717 shards_remaining.remove(shard_index)
718 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700719
maruel@chromium.org0437a732013-08-27 16:05:52 +0000720 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700721 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000722 should_stop.set()
723
724
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400725def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000726 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700727 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400728 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700729 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
730 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400731 else:
732 pending = 'N/A'
733
maruel77f720b2015-09-15 12:35:22 -0700734 if metadata.get('duration') is not None:
735 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400736 else:
737 duration = 'N/A'
738
maruel77f720b2015-09-15 12:35:22 -0700739 if metadata.get('exit_code') is not None:
740 # Integers are encoded as string to not loose precision.
741 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400742 else:
743 exit_code = 'N/A'
744
745 bot_id = metadata.get('bot_id') or 'N/A'
746
maruel77f720b2015-09-15 12:35:22 -0700747 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400748 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400749 tag_footer = (
750 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
751 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400752
753 tag_len = max(len(tag_header), len(tag_footer))
754 dash_pad = '+-%s-+\n' % ('-' * tag_len)
755 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
756 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
757
758 header = dash_pad + tag_header + dash_pad
759 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700760 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400761 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000762
763
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700764def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700765 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700766 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700767 """Retrieves results of a Swarming task.
768
769 Returns:
770 process exit code that should be returned to the user.
771 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700772 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700773 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700774
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700775 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700776 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400777 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700778 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400779 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400780 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700781 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700783
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400784 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700785 shard_exit_code = metadata.get('exit_code')
786 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700787 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700788 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700789 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400790 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700791 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700792
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700793 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400794 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400795 if len(seen_shards) < len(task_ids):
796 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700797 else:
maruel77f720b2015-09-15 12:35:22 -0700798 print('%s: %s %s' % (
799 metadata.get('bot_id', 'N/A'),
800 metadata['task_id'],
801 shard_exit_code))
802 if metadata['output']:
803 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400804 if output:
805 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700806 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700807 summary = output_collector.finalize()
808 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700809 # TODO(maruel): Make this optional.
810 for i in summary['shards']:
811 if i:
812 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700813 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700814
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400815 if decorate and total_duration:
816 print('Total duration: %.1fs' % total_duration)
817
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400818 if len(seen_shards) != len(task_ids):
819 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700820 print >> sys.stderr, ('Results from some shards are missing: %s' %
821 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700822 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700823
maruela5490782015-09-30 10:56:59 -0700824 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000825
826
maruel77f720b2015-09-15 12:35:22 -0700827### API management.
828
829
830class APIError(Exception):
831 pass
832
833
834def endpoints_api_discovery_apis(host):
835 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
836 the APIs exposed by a host.
837
838 https://developers.google.com/discovery/v1/reference/apis/list
839 """
840 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
841 if data is None:
842 raise APIError('Failed to discover APIs on %s' % host)
843 out = {}
844 for api in data['items']:
845 if api['id'] == 'discovery:v1':
846 continue
847 # URL is of the following form:
848 # url = host + (
849 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
850 api_data = net.url_read_json(api['discoveryRestUrl'])
851 if api_data is None:
852 raise APIError('Failed to discover %s on %s' % (api['id'], host))
853 out[api['id']] = api_data
854 return out
855
856
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500857### Commands.
858
859
860def abort_task(_swarming, _manifest):
861 """Given a task manifest that was triggered, aborts its execution."""
862 # TODO(vadimsh): No supported by the server yet.
863
864
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400865def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400866 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500867 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500868 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500869 dest='dimensions', metavar='FOO bar',
870 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500871 parser.add_option_group(parser.filter_group)
872
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400873
Vadim Shtayurab450c602014-05-12 19:23:25 -0700874def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400875 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700876 parser.sharding_group.add_option(
877 '--shards', type='int', default=1,
878 help='Number of shards to trigger and collect.')
879 parser.add_option_group(parser.sharding_group)
880
881
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400882def add_trigger_options(parser):
883 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500884 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400885 add_filter_options(parser)
886
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400887 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500888 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500889 '-s', '--isolated',
890 help='Hash of the .isolated to grab from the isolate server')
891 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500892 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700893 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500894 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500895 '--priority', type='int', default=100,
896 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500897 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500898 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400899 help='Display name of the task. Defaults to '
900 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
901 'isolated file is provided, if a hash is provided, it defaults to '
902 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400903 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400904 '--tags', action='append', default=[],
905 help='Tags to assign to the task.')
906 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500907 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400908 help='User associated with the task. Defaults to authenticated user on '
909 'the server.')
910 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400911 '--idempotent', action='store_true', default=False,
912 help='When set, the server will actively try to find a previous task '
913 'with the same parameter and return this result instead if possible')
914 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400915 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400916 help='Seconds to allow the task to be pending for a bot to run before '
917 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400918 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400919 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400920 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400921 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400922 '--hard-timeout', type='int', default=60*60,
923 help='Seconds to allow the task to complete.')
924 parser.task_group.add_option(
925 '--io-timeout', type='int', default=20*60,
926 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500927 parser.task_group.add_option(
928 '--raw-cmd', action='store_true', default=False,
929 help='When set, the command after -- is used as-is without run_isolated. '
930 'In this case, no .isolated file is expected.')
borenet02f772b2016-06-22 12:42:19 -0700931 parser.task_group.add_option(
932 '--cipd-package', action='append', default=[],
933 help='CIPD packages to install on the Swarming bot. Uses the format: '
934 'path:package_name:version')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500935 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000936
937
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500938def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500939 """Processes trigger options and uploads files to isolate server if necessary.
940 """
941 options.dimensions = dict(options.dimensions)
942 options.env = dict(options.env)
943
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500944 if not options.dimensions:
945 parser.error('Please at least specify one --dimension')
946 if options.raw_cmd:
947 if not args:
948 parser.error(
949 'Arguments with --raw-cmd should be passed after -- as command '
950 'delimiter.')
951 if options.isolate_server:
952 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
953
954 command = args
955 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500956 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500957 options.user,
958 '_'.join(
959 '%s=%s' % (k, v)
960 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700961 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500962 else:
nodir55be77b2016-05-03 09:39:57 -0700963 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500964 try:
maruel77f720b2015-09-15 12:35:22 -0700965 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500966 except ValueError as e:
967 parser.error(str(e))
968
borenet02f772b2016-06-22 12:42:19 -0700969 cipd_packages = []
970 for p in options.cipd_package:
971 split = p.split(':', 2)
972 if len(split) != 3:
973 parser.error('CIPD packages must take the form: path:package:version')
974 cipd_packages.append(CipdPackage(
975 package_name=split[1],
976 path=split[0],
977 version=split[2]))
978 cipd_input = None
979 if cipd_packages:
980 cipd_input = CipdInput(
981 client_package=None,
982 packages=cipd_packages,
983 server=None)
984
nodir152cba62016-05-12 16:08:56 -0700985 # If inputs_ref.isolated is used, command is actually extra_args.
986 # Otherwise it's an actual command to run.
987 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -0700988 properties = TaskProperties(
borenet02f772b2016-06-22 12:42:19 -0700989 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -0700990 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500991 dimensions=options.dimensions,
992 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700993 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -0700994 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -0700995 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500996 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700997 inputs_ref=inputs_ref,
998 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700999 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1000 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -07001001 return NewTaskRequest(
1002 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001003 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001004 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001005 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001006 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001007 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001008 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001009
1010
1011def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001012 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001013 '-t', '--timeout', type='float',
1014 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1015 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001016 parser.group_logging.add_option(
1017 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001018 parser.group_logging.add_option(
1019 '--print-status-updates', action='store_true',
1020 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001021 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001022 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001023 '--task-summary-json',
1024 metavar='FILE',
1025 help='Dump a summary of task results to this file as json. It contains '
1026 'only shards statuses as know to server directly. Any output files '
1027 'emitted by the task can be collected by using --task-output-dir')
1028 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001029 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001030 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001031 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001032 'directory contains per-shard directory with output files produced '
1033 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001034 parser.task_output_group.add_option(
1035 '--perf', action='store_true', default=False,
1036 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001037 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001038
1039
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001040@subcommand.usage('bots...')
1041def CMDbot_delete(parser, args):
1042 """Forcibly deletes bots from the Swarming server."""
1043 parser.add_option(
1044 '-f', '--force', action='store_true',
1045 help='Do not prompt for confirmation')
1046 options, args = parser.parse_args(args)
1047 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001048 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001049
1050 bots = sorted(args)
1051 if not options.force:
1052 print('Delete the following bots?')
1053 for bot in bots:
1054 print(' %s' % bot)
1055 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1056 print('Goodbye.')
1057 return 1
1058
1059 result = 0
1060 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001061 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1062 if net.url_read_json(url, data={}, method='POST') is None:
1063 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001064 result = 1
1065 return result
1066
1067
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001068def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001069 """Returns information about the bots connected to the Swarming server."""
1070 add_filter_options(parser)
1071 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001072 '--dead-only', action='store_true',
1073 help='Only print dead bots, useful to reap them and reimage broken bots')
1074 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001075 '-k', '--keep-dead', action='store_true',
1076 help='Do not filter out dead bots')
1077 parser.filter_group.add_option(
1078 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001079 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001080 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001081
1082 if options.keep_dead and options.dead_only:
1083 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001084
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001085 bots = []
1086 cursor = None
1087 limit = 250
1088 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001089 base_url = (
1090 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001091 while True:
1092 url = base_url
1093 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001094 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001095 data = net.url_read_json(url)
1096 if data is None:
1097 print >> sys.stderr, 'Failed to access %s' % options.swarming
1098 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001099 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001100 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001101 if not cursor:
1102 break
1103
maruel77f720b2015-09-15 12:35:22 -07001104 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001105 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001106 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001107 continue
maruel77f720b2015-09-15 12:35:22 -07001108 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001109 continue
1110
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001111 # If the user requested to filter on dimensions, ensure the bot has all the
1112 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001113 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001114 for key, value in options.dimensions:
1115 if key not in dimensions:
1116 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001117 # A bot can have multiple value for a key, for example,
1118 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1119 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001120 if isinstance(dimensions[key], list):
1121 if value not in dimensions[key]:
1122 break
1123 else:
1124 if value != dimensions[key]:
1125 break
1126 else:
maruel77f720b2015-09-15 12:35:22 -07001127 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001128 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001129 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001130 if bot.get('task_id'):
1131 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001132 return 0
1133
1134
maruelfd0a90c2016-06-10 11:51:10 -07001135@subcommand.usage('task_id')
1136def CMDcancel(parser, args):
1137 """Cancels a task."""
1138 options, args = parser.parse_args(args)
1139 if not args:
1140 parser.error('Please specify the task to cancel')
1141 for task_id in args:
1142 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
1143 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1144 print('Deleting %s failed. Probably already gone' % task_id)
1145 return 1
1146 return 0
1147
1148
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001149@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001150def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001151 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001152
1153 The result can be in multiple part if the execution was sharded. It can
1154 potentially have retries.
1155 """
1156 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001157 parser.add_option(
1158 '-j', '--json',
1159 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001160 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001161 if not args and not options.json:
1162 parser.error('Must specify at least one task id or --json.')
1163 if args and options.json:
1164 parser.error('Only use one of task id or --json.')
1165
1166 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001167 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001168 try:
maruel1ceb3872015-10-14 06:10:44 -07001169 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001170 data = json.load(f)
1171 except (IOError, ValueError):
1172 parser.error('Failed to open %s' % options.json)
1173 try:
1174 tasks = sorted(
1175 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1176 args = [t['task_id'] for t in tasks]
1177 except (KeyError, TypeError):
1178 parser.error('Failed to process %s' % options.json)
1179 if options.timeout is None:
1180 options.timeout = (
1181 data['request']['properties']['execution_timeout_secs'] +
1182 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001183 else:
1184 valid = frozenset('0123456789abcdef')
1185 if any(not valid.issuperset(task_id) for task_id in args):
1186 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001187
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001188 try:
1189 return collect(
1190 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001191 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001192 options.timeout,
1193 options.decorate,
1194 options.print_status_updates,
1195 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001196 options.task_output_dir,
1197 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001198 except Failure:
1199 on_error.report(None)
1200 return 1
1201
1202
maruelbea00862015-09-18 09:55:36 -07001203@subcommand.usage('[filename]')
1204def CMDput_bootstrap(parser, args):
1205 """Uploads a new version of bootstrap.py."""
1206 options, args = parser.parse_args(args)
1207 if len(args) != 1:
1208 parser.error('Must specify file to upload')
1209 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001210 path = unicode(os.path.abspath(args[0]))
1211 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001212 content = f.read().decode('utf-8')
1213 data = net.url_read_json(url, data={'content': content})
1214 print data
1215 return 0
1216
1217
1218@subcommand.usage('[filename]')
1219def CMDput_bot_config(parser, args):
1220 """Uploads a new version of bot_config.py."""
1221 options, args = parser.parse_args(args)
1222 if len(args) != 1:
1223 parser.error('Must specify file to upload')
1224 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001225 path = unicode(os.path.abspath(args[0]))
1226 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001227 content = f.read().decode('utf-8')
1228 data = net.url_read_json(url, data={'content': content})
1229 print data
1230 return 0
1231
1232
maruel77f720b2015-09-15 12:35:22 -07001233@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001234def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001235 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1236 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001237
1238 Examples:
maruel77f720b2015-09-15 12:35:22 -07001239 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001240 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001241
maruel77f720b2015-09-15 12:35:22 -07001242 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001243 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1244
1245 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1246 quoting is important!:
1247 swarming.py query -S server-url.com --limit 10 \\
1248 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001249 """
1250 CHUNK_SIZE = 250
1251
1252 parser.add_option(
1253 '-L', '--limit', type='int', default=200,
1254 help='Limit to enforce on limitless items (like number of tasks); '
1255 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001256 parser.add_option(
1257 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001258 parser.add_option(
1259 '--progress', action='store_true',
1260 help='Prints a dot at each request to show progress')
1261 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001262 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001263 parser.error(
1264 'Must specify only method name and optionally query args properly '
1265 'escaped.')
1266 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001267 url = base_url
1268 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001269 # Check check, change if not working out.
1270 merge_char = '&' if '?' in url else '?'
1271 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001272 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001273 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001274 # TODO(maruel): Do basic diagnostic.
1275 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001276 return 1
1277
1278 # Some items support cursors. Try to get automatically if cursors are needed
1279 # by looking at the 'cursor' items.
1280 while (
1281 data.get('cursor') and
1282 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001283 merge_char = '&' if '?' in base_url else '?'
1284 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001285 if options.limit:
1286 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001287 if options.progress:
1288 sys.stdout.write('.')
1289 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001290 new = net.url_read_json(url)
1291 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001292 if options.progress:
1293 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001294 print >> sys.stderr, 'Failed to access %s' % options.swarming
1295 return 1
maruel81b37132015-10-21 06:42:13 -07001296 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001297 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001298
maruel77f720b2015-09-15 12:35:22 -07001299 if options.progress:
1300 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001301 if options.limit and len(data.get('items', [])) > options.limit:
1302 data['items'] = data['items'][:options.limit]
1303 data.pop('cursor', None)
1304
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001305 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001306 options.json = unicode(os.path.abspath(options.json))
1307 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001308 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001309 try:
maruel77f720b2015-09-15 12:35:22 -07001310 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001311 sys.stdout.write('\n')
1312 except IOError:
1313 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001314 return 0
1315
1316
maruel77f720b2015-09-15 12:35:22 -07001317def CMDquery_list(parser, args):
1318 """Returns list of all the Swarming APIs that can be used with command
1319 'query'.
1320 """
1321 parser.add_option(
1322 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1323 options, args = parser.parse_args(args)
1324 if args:
1325 parser.error('No argument allowed.')
1326
1327 try:
1328 apis = endpoints_api_discovery_apis(options.swarming)
1329 except APIError as e:
1330 parser.error(str(e))
1331 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001332 options.json = unicode(os.path.abspath(options.json))
1333 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001334 json.dump(apis, f)
1335 else:
1336 help_url = (
1337 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1338 options.swarming)
1339 for api_id, api in sorted(apis.iteritems()):
1340 print api_id
1341 print ' ' + api['description']
1342 for resource_name, resource in sorted(api['resources'].iteritems()):
1343 print ''
1344 for method_name, method in sorted(resource['methods'].iteritems()):
1345 # Only list the GET ones.
1346 if method['httpMethod'] != 'GET':
1347 continue
1348 print '- %s.%s: %s' % (
1349 resource_name, method_name, method['path'])
1350 print ' ' + method['description']
1351 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1352 return 0
1353
1354
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001355@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001356def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001357 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001358
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001359 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001360 """
1361 add_trigger_options(parser)
1362 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001363 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001364 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001365 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001366 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001367 tasks = trigger_task_shards(
1368 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001369 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001370 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001371 'Failed to trigger %s(%s): %s' %
1372 (options.task_name, args[0], e.args[0]))
1373 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001374 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001375 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001376 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001377 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001378 task_ids = [
1379 t['task_id']
1380 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1381 ]
maruel71c61c82016-02-22 06:52:05 -08001382 if options.timeout is None:
1383 options.timeout = (
1384 task_request.properties.execution_timeout_secs +
1385 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001386 try:
1387 return collect(
1388 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001389 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001390 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001391 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001392 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001393 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001394 options.task_output_dir,
1395 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001396 except Failure:
1397 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001398 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001399
1400
maruel18122c62015-10-23 06:31:23 -07001401@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001402def CMDreproduce(parser, args):
1403 """Runs a task locally that was triggered on the server.
1404
1405 This running locally the same commands that have been run on the bot. The data
1406 downloaded will be in a subdirectory named 'work' of the current working
1407 directory.
maruel18122c62015-10-23 06:31:23 -07001408
1409 You can pass further additional arguments to the target command by passing
1410 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001411 """
maruelc070e672016-02-22 17:32:57 -08001412 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001413 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001414 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001415 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001416 extra_args = []
1417 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001418 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001419 if len(args) > 1:
1420 if args[1] == '--':
1421 if len(args) > 2:
1422 extra_args = args[2:]
1423 else:
1424 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001425
maruel77f720b2015-09-15 12:35:22 -07001426 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001427 request = net.url_read_json(url)
1428 if not request:
1429 print >> sys.stderr, 'Failed to retrieve request data for the task'
1430 return 1
1431
maruel12e30012015-10-09 11:55:35 -07001432 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001433 if fs.isdir(workdir):
1434 parser.error('Please delete the directory \'work\' first')
1435 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001436
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001437 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001438 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001439 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001440 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001441 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001442 for i in properties['env']:
1443 key = i['key'].encode('utf-8')
1444 if not i['value']:
1445 env.pop(key, None)
1446 else:
1447 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001448
nodir152cba62016-05-12 16:08:56 -07001449 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001450 # Create the tree.
1451 with isolateserver.get_storage(
1452 properties['inputs_ref']['isolatedserver'],
1453 properties['inputs_ref']['namespace']) as storage:
1454 bundle = isolateserver.fetch_isolated(
1455 properties['inputs_ref']['isolated'],
1456 storage,
1457 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001458 workdir,
1459 False)
maruel29ab2fd2015-10-16 11:44:01 -07001460 command = bundle.command
1461 if bundle.relative_cwd:
1462 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001463 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001464 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001465 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001466 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001467 if not options.output_dir and new_command != command:
1468 parser.error('The task has outputs, you must use --output-dir')
1469 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001470 else:
1471 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001472 try:
maruel18122c62015-10-23 06:31:23 -07001473 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001474 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001475 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001476 print >> sys.stderr, str(e)
1477 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001478
1479
maruel0eb1d1b2015-10-02 14:48:21 -07001480@subcommand.usage('bot_id')
1481def CMDterminate(parser, args):
1482 """Tells a bot to gracefully shut itself down as soon as it can.
1483
1484 This is done by completing whatever current task there is then exiting the bot
1485 process.
1486 """
1487 parser.add_option(
1488 '--wait', action='store_true', help='Wait for the bot to terminate')
1489 options, args = parser.parse_args(args)
1490 if len(args) != 1:
1491 parser.error('Please provide the bot id')
1492 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1493 request = net.url_read_json(url, data={})
1494 if not request:
1495 print >> sys.stderr, 'Failed to ask for termination'
1496 return 1
1497 if options.wait:
1498 return collect(
maruel9531ce02016-04-13 06:11:23 -07001499 options.swarming, [request['task_id']], 0., False, False, None, None,
1500 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001501 return 0
1502
1503
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001504@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001505def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001506 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001507
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001508 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001509 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001510
1511 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001512
1513 Passes all extra arguments provided after '--' as additional command line
1514 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001515 """
1516 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001517 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001518 parser.add_option(
1519 '--dump-json',
1520 metavar='FILE',
1521 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001522 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001523 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001524 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001525 tasks = trigger_task_shards(
1526 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001527 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001528 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001529 tasks_sorted = sorted(
1530 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001531 if options.dump_json:
1532 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001533 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001534 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001535 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001536 }
maruel46b015f2015-10-13 18:40:35 -07001537 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001538 print('To collect results, use:')
1539 print(' swarming.py collect -S %s --json %s' %
1540 (options.swarming, options.dump_json))
1541 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001542 print('To collect results, use:')
1543 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001544 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1545 print('Or visit:')
1546 for t in tasks_sorted:
1547 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001548 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001549 except Failure:
1550 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001551 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001552
1553
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001554class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001555 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001556 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001557 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001558 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001559 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001560 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001561 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001562 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001563 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001564 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001565
1566 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001567 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001568 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001569 auth.process_auth_options(self, options)
1570 user = self._process_swarming(options)
1571 if hasattr(options, 'user') and not options.user:
1572 options.user = user
1573 return options, args
1574
1575 def _process_swarming(self, options):
1576 """Processes the --swarming option and aborts if not specified.
1577
1578 Returns the identity as determined by the server.
1579 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001580 if not options.swarming:
1581 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001582 try:
1583 options.swarming = net.fix_url(options.swarming)
1584 except ValueError as e:
1585 self.error('--swarming %s' % e)
1586 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001587 try:
1588 user = auth.ensure_logged_in(options.swarming)
1589 except ValueError as e:
1590 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001591 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001592
1593
1594def main(args):
1595 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001596 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001597
1598
1599if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001600 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001601 fix_encoding.fix_encoding()
1602 tools.disable_buffering()
1603 colorama.init()
1604 sys.exit(main(sys.argv[1:]))