blob: a2987a169e0516c5b6777d625c3dc6af8a8c9f86 [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(
maruel380e3262016-08-31 16:10:06 -0700250 swarming + '/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
maruel380e3262016-08-31 16:10:06 -0700539 result_url = '%s/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'
maruel380e3262016-08-31 16:10:06 -0700542 output_url = '%s/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 """
maruel380e3262016-08-31 16:10:06 -0700840 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
841 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700842 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
843 if data is None:
844 raise APIError('Failed to discover APIs on %s' % host)
845 out = {}
846 for api in data['items']:
847 if api['id'] == 'discovery:v1':
848 continue
849 # URL is of the following form:
850 # url = host + (
851 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
852 api_data = net.url_read_json(api['discoveryRestUrl'])
853 if api_data is None:
854 raise APIError('Failed to discover %s on %s' % (api['id'], host))
855 out[api['id']] = api_data
856 return out
857
858
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500859### Commands.
860
861
862def abort_task(_swarming, _manifest):
863 """Given a task manifest that was triggered, aborts its execution."""
864 # TODO(vadimsh): No supported by the server yet.
865
866
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400867def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400868 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500869 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500870 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500871 dest='dimensions', metavar='FOO bar',
872 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500873 parser.add_option_group(parser.filter_group)
874
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400875
Vadim Shtayurab450c602014-05-12 19:23:25 -0700876def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400877 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700878 parser.sharding_group.add_option(
879 '--shards', type='int', default=1,
880 help='Number of shards to trigger and collect.')
881 parser.add_option_group(parser.sharding_group)
882
883
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400884def add_trigger_options(parser):
885 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500886 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400887 add_filter_options(parser)
888
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400889 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500890 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500891 '-s', '--isolated',
892 help='Hash of the .isolated to grab from the isolate server')
893 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500894 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700895 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500896 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500897 '--priority', type='int', default=100,
898 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500899 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500900 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400901 help='Display name of the task. Defaults to '
902 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
903 'isolated file is provided, if a hash is provided, it defaults to '
904 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400905 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400906 '--tags', action='append', default=[],
907 help='Tags to assign to the task.')
908 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500909 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400910 help='User associated with the task. Defaults to authenticated user on '
911 'the server.')
912 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400913 '--idempotent', action='store_true', default=False,
914 help='When set, the server will actively try to find a previous task '
915 'with the same parameter and return this result instead if possible')
916 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400917 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400918 help='Seconds to allow the task to be pending for a bot to run before '
919 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400920 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400921 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400922 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400923 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400924 '--hard-timeout', type='int', default=60*60,
925 help='Seconds to allow the task to complete.')
926 parser.task_group.add_option(
927 '--io-timeout', type='int', default=20*60,
928 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500929 parser.task_group.add_option(
930 '--raw-cmd', action='store_true', default=False,
931 help='When set, the command after -- is used as-is without run_isolated. '
932 'In this case, no .isolated file is expected.')
borenet02f772b2016-06-22 12:42:19 -0700933 parser.task_group.add_option(
934 '--cipd-package', action='append', default=[],
935 help='CIPD packages to install on the Swarming bot. Uses the format: '
936 'path:package_name:version')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500937 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000938
939
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500940def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500941 """Processes trigger options and uploads files to isolate server if necessary.
942 """
943 options.dimensions = dict(options.dimensions)
944 options.env = dict(options.env)
945
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500946 if not options.dimensions:
947 parser.error('Please at least specify one --dimension')
948 if options.raw_cmd:
949 if not args:
950 parser.error(
951 'Arguments with --raw-cmd should be passed after -- as command '
952 'delimiter.')
953 if options.isolate_server:
954 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
955
956 command = args
957 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500958 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500959 options.user,
960 '_'.join(
961 '%s=%s' % (k, v)
962 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700963 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500964 else:
nodir55be77b2016-05-03 09:39:57 -0700965 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500966 try:
maruel77f720b2015-09-15 12:35:22 -0700967 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500968 except ValueError as e:
969 parser.error(str(e))
970
borenet02f772b2016-06-22 12:42:19 -0700971 cipd_packages = []
972 for p in options.cipd_package:
973 split = p.split(':', 2)
974 if len(split) != 3:
975 parser.error('CIPD packages must take the form: path:package:version')
976 cipd_packages.append(CipdPackage(
977 package_name=split[1],
978 path=split[0],
979 version=split[2]))
980 cipd_input = None
981 if cipd_packages:
982 cipd_input = CipdInput(
983 client_package=None,
984 packages=cipd_packages,
985 server=None)
986
nodir152cba62016-05-12 16:08:56 -0700987 # If inputs_ref.isolated is used, command is actually extra_args.
988 # Otherwise it's an actual command to run.
989 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -0700990 properties = TaskProperties(
borenet02f772b2016-06-22 12:42:19 -0700991 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -0700992 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500993 dimensions=options.dimensions,
994 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700995 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -0700996 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -0700997 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500998 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700999 inputs_ref=inputs_ref,
1000 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -07001001 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1002 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -07001003 return NewTaskRequest(
1004 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001005 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001006 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001007 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001008 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001009 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001010 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001011
1012
1013def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001014 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001015 '-t', '--timeout', type='float',
1016 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1017 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001018 parser.group_logging.add_option(
1019 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001020 parser.group_logging.add_option(
1021 '--print-status-updates', action='store_true',
1022 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001023 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001024 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001025 '--task-summary-json',
1026 metavar='FILE',
1027 help='Dump a summary of task results to this file as json. It contains '
1028 'only shards statuses as know to server directly. Any output files '
1029 'emitted by the task can be collected by using --task-output-dir')
1030 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001031 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001032 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001033 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001034 'directory contains per-shard directory with output files produced '
1035 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001036 parser.task_output_group.add_option(
1037 '--perf', action='store_true', default=False,
1038 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001039 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001040
1041
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001042@subcommand.usage('bots...')
1043def CMDbot_delete(parser, args):
1044 """Forcibly deletes bots from the Swarming server."""
1045 parser.add_option(
1046 '-f', '--force', action='store_true',
1047 help='Do not prompt for confirmation')
1048 options, args = parser.parse_args(args)
1049 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001050 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001051
1052 bots = sorted(args)
1053 if not options.force:
1054 print('Delete the following bots?')
1055 for bot in bots:
1056 print(' %s' % bot)
1057 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1058 print('Goodbye.')
1059 return 1
1060
1061 result = 0
1062 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001063 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001064 if net.url_read_json(url, data={}, method='POST') is None:
1065 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001066 result = 1
1067 return result
1068
1069
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001070def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001071 """Returns information about the bots connected to the Swarming server."""
1072 add_filter_options(parser)
1073 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001074 '--dead-only', action='store_true',
1075 help='Only print dead bots, useful to reap them and reimage broken bots')
1076 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001077 '-k', '--keep-dead', action='store_true',
1078 help='Do not filter out dead bots')
1079 parser.filter_group.add_option(
1080 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001081 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001082 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001083
1084 if options.keep_dead and options.dead_only:
1085 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001086
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001087 bots = []
1088 cursor = None
1089 limit = 250
1090 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001091 base_url = (
maruel380e3262016-08-31 16:10:06 -07001092 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001093 while True:
1094 url = base_url
1095 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001096 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001097 data = net.url_read_json(url)
1098 if data is None:
1099 print >> sys.stderr, 'Failed to access %s' % options.swarming
1100 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001101 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001102 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001103 if not cursor:
1104 break
1105
maruel77f720b2015-09-15 12:35:22 -07001106 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001107 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001108 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001109 continue
maruel77f720b2015-09-15 12:35:22 -07001110 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001111 continue
1112
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001113 # If the user requested to filter on dimensions, ensure the bot has all the
1114 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001115 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001116 for key, value in options.dimensions:
1117 if key not in dimensions:
1118 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001119 # A bot can have multiple value for a key, for example,
1120 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1121 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001122 if isinstance(dimensions[key], list):
1123 if value not in dimensions[key]:
1124 break
1125 else:
1126 if value != dimensions[key]:
1127 break
1128 else:
maruel77f720b2015-09-15 12:35:22 -07001129 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001130 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001131 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001132 if bot.get('task_id'):
1133 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001134 return 0
1135
1136
maruelfd0a90c2016-06-10 11:51:10 -07001137@subcommand.usage('task_id')
1138def CMDcancel(parser, args):
1139 """Cancels a task."""
1140 options, args = parser.parse_args(args)
1141 if not args:
1142 parser.error('Please specify the task to cancel')
1143 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001144 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001145 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1146 print('Deleting %s failed. Probably already gone' % task_id)
1147 return 1
1148 return 0
1149
1150
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001151@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001152def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001153 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001154
1155 The result can be in multiple part if the execution was sharded. It can
1156 potentially have retries.
1157 """
1158 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001159 parser.add_option(
1160 '-j', '--json',
1161 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001162 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001163 if not args and not options.json:
1164 parser.error('Must specify at least one task id or --json.')
1165 if args and options.json:
1166 parser.error('Only use one of task id or --json.')
1167
1168 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001169 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001170 try:
maruel1ceb3872015-10-14 06:10:44 -07001171 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001172 data = json.load(f)
1173 except (IOError, ValueError):
1174 parser.error('Failed to open %s' % options.json)
1175 try:
1176 tasks = sorted(
1177 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1178 args = [t['task_id'] for t in tasks]
1179 except (KeyError, TypeError):
1180 parser.error('Failed to process %s' % options.json)
1181 if options.timeout is None:
1182 options.timeout = (
1183 data['request']['properties']['execution_timeout_secs'] +
1184 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001185 else:
1186 valid = frozenset('0123456789abcdef')
1187 if any(not valid.issuperset(task_id) for task_id in args):
1188 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001189
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001190 try:
1191 return collect(
1192 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001193 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001194 options.timeout,
1195 options.decorate,
1196 options.print_status_updates,
1197 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001198 options.task_output_dir,
1199 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001200 except Failure:
1201 on_error.report(None)
1202 return 1
1203
1204
maruelbea00862015-09-18 09:55:36 -07001205@subcommand.usage('[filename]')
1206def CMDput_bootstrap(parser, args):
1207 """Uploads a new version of bootstrap.py."""
1208 options, args = parser.parse_args(args)
1209 if len(args) != 1:
1210 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001211 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001212 path = unicode(os.path.abspath(args[0]))
1213 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001214 content = f.read().decode('utf-8')
1215 data = net.url_read_json(url, data={'content': content})
1216 print data
1217 return 0
1218
1219
1220@subcommand.usage('[filename]')
1221def CMDput_bot_config(parser, args):
1222 """Uploads a new version of bot_config.py."""
1223 options, args = parser.parse_args(args)
1224 if len(args) != 1:
1225 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001226 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001227 path = unicode(os.path.abspath(args[0]))
1228 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001229 content = f.read().decode('utf-8')
1230 data = net.url_read_json(url, data={'content': content})
1231 print data
1232 return 0
1233
1234
maruel77f720b2015-09-15 12:35:22 -07001235@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001236def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001237 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1238 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001239
1240 Examples:
maruel77f720b2015-09-15 12:35:22 -07001241 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001242 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001243
maruel77f720b2015-09-15 12:35:22 -07001244 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001245 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1246
1247 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1248 quoting is important!:
1249 swarming.py query -S server-url.com --limit 10 \\
1250 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001251 """
1252 CHUNK_SIZE = 250
1253
1254 parser.add_option(
1255 '-L', '--limit', type='int', default=200,
1256 help='Limit to enforce on limitless items (like number of tasks); '
1257 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001258 parser.add_option(
1259 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001260 parser.add_option(
1261 '--progress', action='store_true',
1262 help='Prints a dot at each request to show progress')
1263 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001264 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001265 parser.error(
1266 'Must specify only method name and optionally query args properly '
1267 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001268 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001269 url = base_url
1270 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001271 # Check check, change if not working out.
1272 merge_char = '&' if '?' in url else '?'
1273 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001274 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001275 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001276 # TODO(maruel): Do basic diagnostic.
1277 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001278 return 1
1279
1280 # Some items support cursors. Try to get automatically if cursors are needed
1281 # by looking at the 'cursor' items.
1282 while (
1283 data.get('cursor') and
1284 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001285 merge_char = '&' if '?' in base_url else '?'
1286 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001287 if options.limit:
1288 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001289 if options.progress:
1290 sys.stdout.write('.')
1291 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001292 new = net.url_read_json(url)
1293 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001294 if options.progress:
1295 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001296 print >> sys.stderr, 'Failed to access %s' % options.swarming
1297 return 1
maruel81b37132015-10-21 06:42:13 -07001298 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001299 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001300
maruel77f720b2015-09-15 12:35:22 -07001301 if options.progress:
1302 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001303 if options.limit and len(data.get('items', [])) > options.limit:
1304 data['items'] = data['items'][:options.limit]
1305 data.pop('cursor', None)
1306
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001307 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001308 options.json = unicode(os.path.abspath(options.json))
1309 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001310 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001311 try:
maruel77f720b2015-09-15 12:35:22 -07001312 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001313 sys.stdout.write('\n')
1314 except IOError:
1315 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001316 return 0
1317
1318
maruel77f720b2015-09-15 12:35:22 -07001319def CMDquery_list(parser, args):
1320 """Returns list of all the Swarming APIs that can be used with command
1321 'query'.
1322 """
1323 parser.add_option(
1324 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1325 options, args = parser.parse_args(args)
1326 if args:
1327 parser.error('No argument allowed.')
1328
1329 try:
1330 apis = endpoints_api_discovery_apis(options.swarming)
1331 except APIError as e:
1332 parser.error(str(e))
1333 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001334 options.json = unicode(os.path.abspath(options.json))
1335 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001336 json.dump(apis, f)
1337 else:
1338 help_url = (
1339 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1340 options.swarming)
1341 for api_id, api in sorted(apis.iteritems()):
1342 print api_id
1343 print ' ' + api['description']
1344 for resource_name, resource in sorted(api['resources'].iteritems()):
1345 print ''
1346 for method_name, method in sorted(resource['methods'].iteritems()):
1347 # Only list the GET ones.
1348 if method['httpMethod'] != 'GET':
1349 continue
1350 print '- %s.%s: %s' % (
1351 resource_name, method_name, method['path'])
1352 print ' ' + method['description']
1353 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1354 return 0
1355
1356
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001357@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001358def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001359 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001360
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001361 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001362 """
1363 add_trigger_options(parser)
1364 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001365 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001366 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001367 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001368 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001369 tasks = trigger_task_shards(
1370 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001371 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001372 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001373 'Failed to trigger %s(%s): %s' %
1374 (options.task_name, args[0], e.args[0]))
1375 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001376 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001377 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001378 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001379 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001380 task_ids = [
1381 t['task_id']
1382 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1383 ]
maruel71c61c82016-02-22 06:52:05 -08001384 if options.timeout is None:
1385 options.timeout = (
1386 task_request.properties.execution_timeout_secs +
1387 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001388 try:
1389 return collect(
1390 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001391 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001392 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001393 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001394 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001395 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001396 options.task_output_dir,
1397 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001398 except Failure:
1399 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001400 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001401
1402
maruel18122c62015-10-23 06:31:23 -07001403@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001404def CMDreproduce(parser, args):
1405 """Runs a task locally that was triggered on the server.
1406
1407 This running locally the same commands that have been run on the bot. The data
1408 downloaded will be in a subdirectory named 'work' of the current working
1409 directory.
maruel18122c62015-10-23 06:31:23 -07001410
1411 You can pass further additional arguments to the target command by passing
1412 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001413 """
maruelc070e672016-02-22 17:32:57 -08001414 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001415 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001416 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001417 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001418 extra_args = []
1419 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001420 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001421 if len(args) > 1:
1422 if args[1] == '--':
1423 if len(args) > 2:
1424 extra_args = args[2:]
1425 else:
1426 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001427
maruel380e3262016-08-31 16:10:06 -07001428 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001429 request = net.url_read_json(url)
1430 if not request:
1431 print >> sys.stderr, 'Failed to retrieve request data for the task'
1432 return 1
1433
maruel12e30012015-10-09 11:55:35 -07001434 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001435 if fs.isdir(workdir):
1436 parser.error('Please delete the directory \'work\' first')
1437 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001438
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001439 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001440 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001441 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001442 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001443 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001444 for i in properties['env']:
1445 key = i['key'].encode('utf-8')
1446 if not i['value']:
1447 env.pop(key, None)
1448 else:
1449 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001450
nodir152cba62016-05-12 16:08:56 -07001451 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001452 # Create the tree.
1453 with isolateserver.get_storage(
1454 properties['inputs_ref']['isolatedserver'],
1455 properties['inputs_ref']['namespace']) as storage:
1456 bundle = isolateserver.fetch_isolated(
1457 properties['inputs_ref']['isolated'],
1458 storage,
1459 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001460 workdir,
1461 False)
maruel29ab2fd2015-10-16 11:44:01 -07001462 command = bundle.command
1463 if bundle.relative_cwd:
1464 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001465 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001466 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001467 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001468 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001469 if not options.output_dir and new_command != command:
1470 parser.error('The task has outputs, you must use --output-dir')
1471 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001472 else:
1473 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001474 try:
maruel18122c62015-10-23 06:31:23 -07001475 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001476 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001477 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001478 print >> sys.stderr, str(e)
1479 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001480
1481
maruel0eb1d1b2015-10-02 14:48:21 -07001482@subcommand.usage('bot_id')
1483def CMDterminate(parser, args):
1484 """Tells a bot to gracefully shut itself down as soon as it can.
1485
1486 This is done by completing whatever current task there is then exiting the bot
1487 process.
1488 """
1489 parser.add_option(
1490 '--wait', action='store_true', help='Wait for the bot to terminate')
1491 options, args = parser.parse_args(args)
1492 if len(args) != 1:
1493 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001494 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001495 request = net.url_read_json(url, data={})
1496 if not request:
1497 print >> sys.stderr, 'Failed to ask for termination'
1498 return 1
1499 if options.wait:
1500 return collect(
maruel9531ce02016-04-13 06:11:23 -07001501 options.swarming, [request['task_id']], 0., False, False, None, None,
1502 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001503 return 0
1504
1505
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001506@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001507def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001508 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001509
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001510 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001511 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001512
1513 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001514
1515 Passes all extra arguments provided after '--' as additional command line
1516 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001517 """
1518 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001519 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001520 parser.add_option(
1521 '--dump-json',
1522 metavar='FILE',
1523 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001524 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001525 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001526 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001527 tasks = trigger_task_shards(
1528 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001529 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001530 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001531 tasks_sorted = sorted(
1532 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001533 if options.dump_json:
1534 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001535 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001536 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001537 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001538 }
maruel46b015f2015-10-13 18:40:35 -07001539 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001540 print('To collect results, use:')
1541 print(' swarming.py collect -S %s --json %s' %
1542 (options.swarming, options.dump_json))
1543 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001544 print('To collect results, use:')
1545 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001546 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1547 print('Or visit:')
1548 for t in tasks_sorted:
1549 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001550 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001551 except Failure:
1552 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001553 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001554
1555
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001556class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001557 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001558 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001559 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001560 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001561 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001562 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001563 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001564 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001565 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001566 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001567
1568 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001569 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001570 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001571 auth.process_auth_options(self, options)
1572 user = self._process_swarming(options)
1573 if hasattr(options, 'user') and not options.user:
1574 options.user = user
1575 return options, args
1576
1577 def _process_swarming(self, options):
1578 """Processes the --swarming option and aborts if not specified.
1579
1580 Returns the identity as determined by the server.
1581 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001582 if not options.swarming:
1583 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001584 try:
1585 options.swarming = net.fix_url(options.swarming)
1586 except ValueError as e:
1587 self.error('--swarming %s' % e)
1588 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001589 try:
1590 user = auth.ensure_logged_in(options.swarming)
1591 except ValueError as e:
1592 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001593 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001594
1595
1596def main(args):
1597 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001598 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001599
1600
1601if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001602 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001603 fix_encoding.fix_encoding()
1604 tools.disable_buffering()
1605 colorama.init()
1606 sys.exit(main(sys.argv[1:]))