blob: cc98e08821984f97fa3696d6358f42ccfc8c7f63 [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),
maruelb8d88d12016-04-08 12:54:01 -0700468 os.path.join(self.task_output_dir, str(shard_index)))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700469
470 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700471 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700472 with self._lock:
473 # Write an array of shard results with None for missing shards.
474 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700475 'shards': [
476 self._per_shard_results.get(i) for i in xrange(self.shard_count)
477 ],
478 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700479 # Write summary.json to task_output_dir as well.
480 if self.task_output_dir:
481 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700482 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700483 summary,
484 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700485 if self._storage:
486 self._storage.close()
487 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700488 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700489
490 def _get_storage(self, isolate_server, namespace):
491 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700492 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700493 with self._lock:
494 if not self._storage:
495 self._storage = isolateserver.get_storage(isolate_server, namespace)
496 else:
497 # Shards must all use exact same isolate server and namespace.
498 if self._storage.location != isolate_server:
499 logging.error(
500 'Task shards are using multiple isolate servers: %s and %s',
501 self._storage.location, isolate_server)
502 return None
503 if self._storage.namespace != namespace:
504 logging.error(
505 'Task shards are using multiple namespaces: %s and %s',
506 self._storage.namespace, namespace)
507 return None
508 return self._storage
509
510
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500511def now():
512 """Exists so it can be mocked easily."""
513 return time.time()
514
515
maruel77f720b2015-09-15 12:35:22 -0700516def parse_time(value):
517 """Converts serialized time from the API to datetime.datetime."""
518 # When microseconds are 0, the '.123456' suffix is elided. This means the
519 # serialized format is not consistent, which confuses the hell out of python.
520 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
521 try:
522 return datetime.datetime.strptime(value, fmt)
523 except ValueError:
524 pass
525 raise ValueError('Failed to parse %s' % value)
526
527
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700528def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700529 base_url, shard_index, task_id, timeout, should_stop, output_collector,
530 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700532
Vadim Shtayurab450c602014-05-12 19:23:25 -0700533 Returns:
534 <result dict> on success.
535 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700536 """
maruel71c61c82016-02-22 06:52:05 -0800537 assert timeout is None or isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700538 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700539 if include_perf:
540 result_url += '?include_performance_stats=true'
maruel77f720b2015-09-15 12:35:22 -0700541 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700542 started = now()
543 deadline = started + timeout if timeout else None
544 attempt = 0
545
546 while not should_stop.is_set():
547 attempt += 1
548
549 # Waiting for too long -> give up.
550 current_time = now()
551 if deadline and current_time >= deadline:
552 logging.error('retrieve_results(%s) timed out on attempt %d',
553 base_url, attempt)
554 return None
555
556 # Do not spin too fast. Spin faster at the beginning though.
557 # Start with 1 sec delay and for each 30 sec of waiting add another second
558 # of delay, until hitting 15 sec ceiling.
559 if attempt > 1:
560 max_delay = min(15, 1 + (current_time - started) / 30.0)
561 delay = min(max_delay, deadline - current_time) if deadline else max_delay
562 if delay > 0:
563 logging.debug('Waiting %.1f sec before retrying', delay)
564 should_stop.wait(delay)
565 if should_stop.is_set():
566 return None
567
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400568 # Disable internal retries in net.url_read_json, since we are doing retries
569 # ourselves.
570 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700571 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
572 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400573 result = net.url_read_json(result_url, retry_50x=False)
574 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400575 continue
maruel77f720b2015-09-15 12:35:22 -0700576
maruelbf53e042015-12-01 15:00:51 -0800577 if result.get('error'):
578 # An error occurred.
579 if result['error'].get('errors'):
580 for err in result['error']['errors']:
581 logging.warning(
582 'Error while reading task: %s; %s',
583 err.get('message'), err.get('debugInfo'))
584 elif result['error'].get('message'):
585 logging.warning(
586 'Error while reading task: %s', result['error']['message'])
587 continue
588
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400589 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700590 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400591 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700592 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700593 # Record the result, try to fetch attached output files (if any).
594 if output_collector:
595 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700596 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700597 if result.get('internal_failure'):
598 logging.error('Internal error!')
599 elif result['state'] == 'BOT_DIED':
600 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700601 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000602
603
maruel77f720b2015-09-15 12:35:22 -0700604def convert_to_old_format(result):
605 """Converts the task result data from Endpoints API format to old API format
606 for compatibility.
607
608 This goes into the file generated as --task-summary-json.
609 """
610 # Sets default.
611 result.setdefault('abandoned_ts', None)
612 result.setdefault('bot_id', None)
613 result.setdefault('bot_version', None)
614 result.setdefault('children_task_ids', [])
615 result.setdefault('completed_ts', None)
616 result.setdefault('cost_saved_usd', None)
617 result.setdefault('costs_usd', None)
618 result.setdefault('deduped_from', None)
619 result.setdefault('name', None)
620 result.setdefault('outputs_ref', None)
621 result.setdefault('properties_hash', None)
622 result.setdefault('server_versions', None)
623 result.setdefault('started_ts', None)
624 result.setdefault('tags', None)
625 result.setdefault('user', None)
626
627 # Convertion back to old API.
628 duration = result.pop('duration', None)
629 result['durations'] = [duration] if duration else []
630 exit_code = result.pop('exit_code', None)
631 result['exit_codes'] = [int(exit_code)] if exit_code else []
632 result['id'] = result.pop('task_id')
633 result['isolated_out'] = result.get('outputs_ref', None)
634 output = result.pop('output', None)
635 result['outputs'] = [output] if output else []
636 # properties_hash
637 # server_version
638 # Endpoints result 'state' as string. For compatibility with old code, convert
639 # to int.
640 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700641 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700642 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700643 if 'bot_dimensions' in result:
644 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700645 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700646 }
647 else:
648 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700649
650
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700651def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400652 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700653 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500654 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000655
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700656 Duplicate shards are ignored. Shards are yielded in order of completion.
657 Timed out shards are NOT yielded at all. Caller can compare number of yielded
658 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000659
660 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500661 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 +0000662 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500663
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700664 output_collector is an optional instance of TaskOutputCollector that will be
665 used to fetch files produced by a task from isolate server to the local disk.
666
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500667 Yields:
668 (index, result). In particular, 'result' is defined as the
669 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000670 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000671 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400672 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700673 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700675
maruel@chromium.org0437a732013-08-27 16:05:52 +0000676 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
677 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700678 # Adds a task to the thread pool to call 'retrieve_results' and return
679 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400680 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700681 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000682 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400683 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700684 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700685
686 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400687 for shard_index, task_id in enumerate(task_ids):
688 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700689
690 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400691 shards_remaining = range(len(task_ids))
692 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700693 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700694 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700695 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700696 shard_index, result = results_channel.pull(
697 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700698 except threading_utils.TaskChannel.Timeout:
699 if print_status_updates:
700 print(
701 'Waiting for results from the following shards: %s' %
702 ', '.join(map(str, shards_remaining)))
703 sys.stdout.flush()
704 continue
705 except Exception:
706 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700707
708 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700709 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000710 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500711 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000712 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700713
Vadim Shtayurab450c602014-05-12 19:23:25 -0700714 # Yield back results to the caller.
715 assert shard_index in shards_remaining
716 shards_remaining.remove(shard_index)
717 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700718
maruel@chromium.org0437a732013-08-27 16:05:52 +0000719 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700720 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000721 should_stop.set()
722
723
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400724def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000725 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700726 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400727 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700728 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
729 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400730 else:
731 pending = 'N/A'
732
maruel77f720b2015-09-15 12:35:22 -0700733 if metadata.get('duration') is not None:
734 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400735 else:
736 duration = 'N/A'
737
maruel77f720b2015-09-15 12:35:22 -0700738 if metadata.get('exit_code') is not None:
739 # Integers are encoded as string to not loose precision.
740 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400741 else:
742 exit_code = 'N/A'
743
744 bot_id = metadata.get('bot_id') or 'N/A'
745
maruel77f720b2015-09-15 12:35:22 -0700746 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400747 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400748 tag_footer = (
749 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
750 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400751
752 tag_len = max(len(tag_header), len(tag_footer))
753 dash_pad = '+-%s-+\n' % ('-' * tag_len)
754 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
755 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
756
757 header = dash_pad + tag_header + dash_pad
758 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700759 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400760 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000761
762
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700763def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700764 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700765 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700766 """Retrieves results of a Swarming task.
767
768 Returns:
769 process exit code that should be returned to the user.
770 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700771 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700772 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700774 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700775 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400776 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700777 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400778 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400779 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700780 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700781 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700782
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400783 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700784 shard_exit_code = metadata.get('exit_code')
785 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700786 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700787 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700788 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400789 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700790 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700791
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700792 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400793 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400794 if len(seen_shards) < len(task_ids):
795 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700796 else:
maruel77f720b2015-09-15 12:35:22 -0700797 print('%s: %s %s' % (
798 metadata.get('bot_id', 'N/A'),
799 metadata['task_id'],
800 shard_exit_code))
801 if metadata['output']:
802 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400803 if output:
804 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700805 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700806 summary = output_collector.finalize()
807 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700808 # TODO(maruel): Make this optional.
809 for i in summary['shards']:
810 if i:
811 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700812 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700813
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400814 if decorate and total_duration:
815 print('Total duration: %.1fs' % total_duration)
816
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400817 if len(seen_shards) != len(task_ids):
818 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700819 print >> sys.stderr, ('Results from some shards are missing: %s' %
820 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700821 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700822
maruela5490782015-09-30 10:56:59 -0700823 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000824
825
maruel77f720b2015-09-15 12:35:22 -0700826### API management.
827
828
829class APIError(Exception):
830 pass
831
832
833def endpoints_api_discovery_apis(host):
834 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
835 the APIs exposed by a host.
836
837 https://developers.google.com/discovery/v1/reference/apis/list
838 """
839 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
840 if data is None:
841 raise APIError('Failed to discover APIs on %s' % host)
842 out = {}
843 for api in data['items']:
844 if api['id'] == 'discovery:v1':
845 continue
846 # URL is of the following form:
847 # url = host + (
848 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
849 api_data = net.url_read_json(api['discoveryRestUrl'])
850 if api_data is None:
851 raise APIError('Failed to discover %s on %s' % (api['id'], host))
852 out[api['id']] = api_data
853 return out
854
855
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500856### Commands.
857
858
859def abort_task(_swarming, _manifest):
860 """Given a task manifest that was triggered, aborts its execution."""
861 # TODO(vadimsh): No supported by the server yet.
862
863
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400864def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400865 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500866 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500867 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500868 dest='dimensions', metavar='FOO bar',
869 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500870 parser.add_option_group(parser.filter_group)
871
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400872
Vadim Shtayurab450c602014-05-12 19:23:25 -0700873def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400874 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700875 parser.sharding_group.add_option(
876 '--shards', type='int', default=1,
877 help='Number of shards to trigger and collect.')
878 parser.add_option_group(parser.sharding_group)
879
880
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400881def add_trigger_options(parser):
882 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500883 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400884 add_filter_options(parser)
885
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400886 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500887 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500888 '-s', '--isolated',
889 help='Hash of the .isolated to grab from the isolate server')
890 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500891 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700892 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500893 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500894 '--priority', type='int', default=100,
895 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500896 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500897 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400898 help='Display name of the task. Defaults to '
899 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
900 'isolated file is provided, if a hash is provided, it defaults to '
901 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400902 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400903 '--tags', action='append', default=[],
904 help='Tags to assign to the task.')
905 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500906 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400907 help='User associated with the task. Defaults to authenticated user on '
908 'the server.')
909 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400910 '--idempotent', action='store_true', default=False,
911 help='When set, the server will actively try to find a previous task '
912 'with the same parameter and return this result instead if possible')
913 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400914 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400915 help='Seconds to allow the task to be pending for a bot to run before '
916 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400917 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400918 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400919 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400920 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400921 '--hard-timeout', type='int', default=60*60,
922 help='Seconds to allow the task to complete.')
923 parser.task_group.add_option(
924 '--io-timeout', type='int', default=20*60,
925 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500926 parser.task_group.add_option(
927 '--raw-cmd', action='store_true', default=False,
928 help='When set, the command after -- is used as-is without run_isolated. '
929 'In this case, no .isolated file is expected.')
borenet02f772b2016-06-22 12:42:19 -0700930 parser.task_group.add_option(
931 '--cipd-package', action='append', default=[],
932 help='CIPD packages to install on the Swarming bot. Uses the format: '
933 'path:package_name:version')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500934 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000935
936
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500937def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500938 """Processes trigger options and uploads files to isolate server if necessary.
939 """
940 options.dimensions = dict(options.dimensions)
941 options.env = dict(options.env)
942
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500943 if not options.dimensions:
944 parser.error('Please at least specify one --dimension')
945 if options.raw_cmd:
946 if not args:
947 parser.error(
948 'Arguments with --raw-cmd should be passed after -- as command '
949 'delimiter.')
950 if options.isolate_server:
951 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
952
953 command = args
954 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500955 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500956 options.user,
957 '_'.join(
958 '%s=%s' % (k, v)
959 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700960 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500961 else:
nodir55be77b2016-05-03 09:39:57 -0700962 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500963 try:
maruel77f720b2015-09-15 12:35:22 -0700964 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500965 except ValueError as e:
966 parser.error(str(e))
967
borenet02f772b2016-06-22 12:42:19 -0700968 cipd_packages = []
969 for p in options.cipd_package:
970 split = p.split(':', 2)
971 if len(split) != 3:
972 parser.error('CIPD packages must take the form: path:package:version')
973 cipd_packages.append(CipdPackage(
974 package_name=split[1],
975 path=split[0],
976 version=split[2]))
977 cipd_input = None
978 if cipd_packages:
979 cipd_input = CipdInput(
980 client_package=None,
981 packages=cipd_packages,
982 server=None)
983
nodir152cba62016-05-12 16:08:56 -0700984 # If inputs_ref.isolated is used, command is actually extra_args.
985 # Otherwise it's an actual command to run.
986 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -0700987 properties = TaskProperties(
borenet02f772b2016-06-22 12:42:19 -0700988 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -0700989 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500990 dimensions=options.dimensions,
991 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700992 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -0700993 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -0700994 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500995 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700996 inputs_ref=inputs_ref,
997 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700998 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
999 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -07001000 return NewTaskRequest(
1001 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001002 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001003 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001004 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001005 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001006 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001007 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001008
1009
1010def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001011 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001012 '-t', '--timeout', type='float',
1013 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1014 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001015 parser.group_logging.add_option(
1016 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001017 parser.group_logging.add_option(
1018 '--print-status-updates', action='store_true',
1019 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001020 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001021 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001022 '--task-summary-json',
1023 metavar='FILE',
1024 help='Dump a summary of task results to this file as json. It contains '
1025 'only shards statuses as know to server directly. Any output files '
1026 'emitted by the task can be collected by using --task-output-dir')
1027 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001028 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001029 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001030 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001031 'directory contains per-shard directory with output files produced '
1032 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001033 parser.task_output_group.add_option(
1034 '--perf', action='store_true', default=False,
1035 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001036 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001037
1038
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001039@subcommand.usage('bots...')
1040def CMDbot_delete(parser, args):
1041 """Forcibly deletes bots from the Swarming server."""
1042 parser.add_option(
1043 '-f', '--force', action='store_true',
1044 help='Do not prompt for confirmation')
1045 options, args = parser.parse_args(args)
1046 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001047 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001048
1049 bots = sorted(args)
1050 if not options.force:
1051 print('Delete the following bots?')
1052 for bot in bots:
1053 print(' %s' % bot)
1054 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1055 print('Goodbye.')
1056 return 1
1057
1058 result = 0
1059 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001060 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1061 if net.url_read_json(url, data={}, method='POST') is None:
1062 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001063 result = 1
1064 return result
1065
1066
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001067def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001068 """Returns information about the bots connected to the Swarming server."""
1069 add_filter_options(parser)
1070 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001071 '--dead-only', action='store_true',
1072 help='Only print dead bots, useful to reap them and reimage broken bots')
1073 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001074 '-k', '--keep-dead', action='store_true',
1075 help='Do not filter out dead bots')
1076 parser.filter_group.add_option(
1077 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001078 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001079 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001080
1081 if options.keep_dead and options.dead_only:
1082 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001083
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001084 bots = []
1085 cursor = None
1086 limit = 250
1087 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001088 base_url = (
1089 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001090 while True:
1091 url = base_url
1092 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001093 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001094 data = net.url_read_json(url)
1095 if data is None:
1096 print >> sys.stderr, 'Failed to access %s' % options.swarming
1097 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001098 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001099 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001100 if not cursor:
1101 break
1102
maruel77f720b2015-09-15 12:35:22 -07001103 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001104 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001105 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001106 continue
maruel77f720b2015-09-15 12:35:22 -07001107 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001108 continue
1109
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001110 # If the user requested to filter on dimensions, ensure the bot has all the
1111 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001112 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001113 for key, value in options.dimensions:
1114 if key not in dimensions:
1115 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001116 # A bot can have multiple value for a key, for example,
1117 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1118 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001119 if isinstance(dimensions[key], list):
1120 if value not in dimensions[key]:
1121 break
1122 else:
1123 if value != dimensions[key]:
1124 break
1125 else:
maruel77f720b2015-09-15 12:35:22 -07001126 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001127 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001128 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001129 if bot.get('task_id'):
1130 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001131 return 0
1132
1133
maruelfd0a90c2016-06-10 11:51:10 -07001134@subcommand.usage('task_id')
1135def CMDcancel(parser, args):
1136 """Cancels a task."""
1137 options, args = parser.parse_args(args)
1138 if not args:
1139 parser.error('Please specify the task to cancel')
1140 for task_id in args:
1141 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
1142 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1143 print('Deleting %s failed. Probably already gone' % task_id)
1144 return 1
1145 return 0
1146
1147
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001148@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001149def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001150 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001151
1152 The result can be in multiple part if the execution was sharded. It can
1153 potentially have retries.
1154 """
1155 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001156 parser.add_option(
1157 '-j', '--json',
1158 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001159 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001160 if not args and not options.json:
1161 parser.error('Must specify at least one task id or --json.')
1162 if args and options.json:
1163 parser.error('Only use one of task id or --json.')
1164
1165 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001166 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001167 try:
maruel1ceb3872015-10-14 06:10:44 -07001168 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001169 data = json.load(f)
1170 except (IOError, ValueError):
1171 parser.error('Failed to open %s' % options.json)
1172 try:
1173 tasks = sorted(
1174 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1175 args = [t['task_id'] for t in tasks]
1176 except (KeyError, TypeError):
1177 parser.error('Failed to process %s' % options.json)
1178 if options.timeout is None:
1179 options.timeout = (
1180 data['request']['properties']['execution_timeout_secs'] +
1181 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001182 else:
1183 valid = frozenset('0123456789abcdef')
1184 if any(not valid.issuperset(task_id) for task_id in args):
1185 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001186
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001187 try:
1188 return collect(
1189 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001190 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001191 options.timeout,
1192 options.decorate,
1193 options.print_status_updates,
1194 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001195 options.task_output_dir,
1196 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001197 except Failure:
1198 on_error.report(None)
1199 return 1
1200
1201
maruelbea00862015-09-18 09:55:36 -07001202@subcommand.usage('[filename]')
1203def CMDput_bootstrap(parser, args):
1204 """Uploads a new version of bootstrap.py."""
1205 options, args = parser.parse_args(args)
1206 if len(args) != 1:
1207 parser.error('Must specify file to upload')
1208 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001209 path = unicode(os.path.abspath(args[0]))
1210 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001211 content = f.read().decode('utf-8')
1212 data = net.url_read_json(url, data={'content': content})
1213 print data
1214 return 0
1215
1216
1217@subcommand.usage('[filename]')
1218def CMDput_bot_config(parser, args):
1219 """Uploads a new version of bot_config.py."""
1220 options, args = parser.parse_args(args)
1221 if len(args) != 1:
1222 parser.error('Must specify file to upload')
1223 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001224 path = unicode(os.path.abspath(args[0]))
1225 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001226 content = f.read().decode('utf-8')
1227 data = net.url_read_json(url, data={'content': content})
1228 print data
1229 return 0
1230
1231
maruel77f720b2015-09-15 12:35:22 -07001232@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001233def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001234 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1235 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001236
1237 Examples:
maruel77f720b2015-09-15 12:35:22 -07001238 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001239 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001240
maruel77f720b2015-09-15 12:35:22 -07001241 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001242 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1243
1244 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1245 quoting is important!:
1246 swarming.py query -S server-url.com --limit 10 \\
1247 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001248 """
1249 CHUNK_SIZE = 250
1250
1251 parser.add_option(
1252 '-L', '--limit', type='int', default=200,
1253 help='Limit to enforce on limitless items (like number of tasks); '
1254 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001255 parser.add_option(
1256 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001257 parser.add_option(
1258 '--progress', action='store_true',
1259 help='Prints a dot at each request to show progress')
1260 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001261 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001262 parser.error(
1263 'Must specify only method name and optionally query args properly '
1264 'escaped.')
1265 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001266 url = base_url
1267 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001268 # Check check, change if not working out.
1269 merge_char = '&' if '?' in url else '?'
1270 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001271 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001272 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001273 # TODO(maruel): Do basic diagnostic.
1274 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001275 return 1
1276
1277 # Some items support cursors. Try to get automatically if cursors are needed
1278 # by looking at the 'cursor' items.
1279 while (
1280 data.get('cursor') and
1281 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001282 merge_char = '&' if '?' in base_url else '?'
1283 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001284 if options.limit:
1285 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001286 if options.progress:
1287 sys.stdout.write('.')
1288 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001289 new = net.url_read_json(url)
1290 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001291 if options.progress:
1292 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001293 print >> sys.stderr, 'Failed to access %s' % options.swarming
1294 return 1
maruel81b37132015-10-21 06:42:13 -07001295 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001296 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001297
maruel77f720b2015-09-15 12:35:22 -07001298 if options.progress:
1299 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001300 if options.limit and len(data.get('items', [])) > options.limit:
1301 data['items'] = data['items'][:options.limit]
1302 data.pop('cursor', None)
1303
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001304 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001305 options.json = unicode(os.path.abspath(options.json))
1306 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001307 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001308 try:
maruel77f720b2015-09-15 12:35:22 -07001309 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001310 sys.stdout.write('\n')
1311 except IOError:
1312 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001313 return 0
1314
1315
maruel77f720b2015-09-15 12:35:22 -07001316def CMDquery_list(parser, args):
1317 """Returns list of all the Swarming APIs that can be used with command
1318 'query'.
1319 """
1320 parser.add_option(
1321 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1322 options, args = parser.parse_args(args)
1323 if args:
1324 parser.error('No argument allowed.')
1325
1326 try:
1327 apis = endpoints_api_discovery_apis(options.swarming)
1328 except APIError as e:
1329 parser.error(str(e))
1330 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001331 options.json = unicode(os.path.abspath(options.json))
1332 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001333 json.dump(apis, f)
1334 else:
1335 help_url = (
1336 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1337 options.swarming)
1338 for api_id, api in sorted(apis.iteritems()):
1339 print api_id
1340 print ' ' + api['description']
1341 for resource_name, resource in sorted(api['resources'].iteritems()):
1342 print ''
1343 for method_name, method in sorted(resource['methods'].iteritems()):
1344 # Only list the GET ones.
1345 if method['httpMethod'] != 'GET':
1346 continue
1347 print '- %s.%s: %s' % (
1348 resource_name, method_name, method['path'])
1349 print ' ' + method['description']
1350 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1351 return 0
1352
1353
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001354@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001355def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001356 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001357
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001358 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001359 """
1360 add_trigger_options(parser)
1361 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001362 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001363 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001364 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001365 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001366 tasks = trigger_task_shards(
1367 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001368 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001369 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001370 'Failed to trigger %s(%s): %s' %
1371 (options.task_name, args[0], e.args[0]))
1372 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001373 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001374 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001375 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001376 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001377 task_ids = [
1378 t['task_id']
1379 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1380 ]
maruel71c61c82016-02-22 06:52:05 -08001381 if options.timeout is None:
1382 options.timeout = (
1383 task_request.properties.execution_timeout_secs +
1384 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001385 try:
1386 return collect(
1387 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001388 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001389 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001390 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001391 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001392 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001393 options.task_output_dir,
1394 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001395 except Failure:
1396 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001397 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001398
1399
maruel18122c62015-10-23 06:31:23 -07001400@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001401def CMDreproduce(parser, args):
1402 """Runs a task locally that was triggered on the server.
1403
1404 This running locally the same commands that have been run on the bot. The data
1405 downloaded will be in a subdirectory named 'work' of the current working
1406 directory.
maruel18122c62015-10-23 06:31:23 -07001407
1408 You can pass further additional arguments to the target command by passing
1409 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001410 """
maruelc070e672016-02-22 17:32:57 -08001411 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001412 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001413 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001414 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001415 extra_args = []
1416 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001417 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001418 if len(args) > 1:
1419 if args[1] == '--':
1420 if len(args) > 2:
1421 extra_args = args[2:]
1422 else:
1423 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001424
maruel77f720b2015-09-15 12:35:22 -07001425 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001426 request = net.url_read_json(url)
1427 if not request:
1428 print >> sys.stderr, 'Failed to retrieve request data for the task'
1429 return 1
1430
maruel12e30012015-10-09 11:55:35 -07001431 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001432 if fs.isdir(workdir):
1433 parser.error('Please delete the directory \'work\' first')
1434 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001435
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001436 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001437 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001438 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001439 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001440 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001441 for i in properties['env']:
1442 key = i['key'].encode('utf-8')
1443 if not i['value']:
1444 env.pop(key, None)
1445 else:
1446 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001447
nodir152cba62016-05-12 16:08:56 -07001448 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001449 # Create the tree.
1450 with isolateserver.get_storage(
1451 properties['inputs_ref']['isolatedserver'],
1452 properties['inputs_ref']['namespace']) as storage:
1453 bundle = isolateserver.fetch_isolated(
1454 properties['inputs_ref']['isolated'],
1455 storage,
1456 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -07001457 workdir)
maruel29ab2fd2015-10-16 11:44:01 -07001458 command = bundle.command
1459 if bundle.relative_cwd:
1460 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001461 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001462 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001463 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001464 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001465 if not options.output_dir and new_command != command:
1466 parser.error('The task has outputs, you must use --output-dir')
1467 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001468 else:
1469 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001470 try:
maruel18122c62015-10-23 06:31:23 -07001471 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001472 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001473 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001474 print >> sys.stderr, str(e)
1475 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001476
1477
maruel0eb1d1b2015-10-02 14:48:21 -07001478@subcommand.usage('bot_id')
1479def CMDterminate(parser, args):
1480 """Tells a bot to gracefully shut itself down as soon as it can.
1481
1482 This is done by completing whatever current task there is then exiting the bot
1483 process.
1484 """
1485 parser.add_option(
1486 '--wait', action='store_true', help='Wait for the bot to terminate')
1487 options, args = parser.parse_args(args)
1488 if len(args) != 1:
1489 parser.error('Please provide the bot id')
1490 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1491 request = net.url_read_json(url, data={})
1492 if not request:
1493 print >> sys.stderr, 'Failed to ask for termination'
1494 return 1
1495 if options.wait:
1496 return collect(
maruel9531ce02016-04-13 06:11:23 -07001497 options.swarming, [request['task_id']], 0., False, False, None, None,
1498 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001499 return 0
1500
1501
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001502@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001503def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001504 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001505
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001506 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001507 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001508
1509 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001510
1511 Passes all extra arguments provided after '--' as additional command line
1512 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001513 """
1514 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001515 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001516 parser.add_option(
1517 '--dump-json',
1518 metavar='FILE',
1519 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001520 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001521 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001522 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001523 tasks = trigger_task_shards(
1524 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001525 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001526 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001527 tasks_sorted = sorted(
1528 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001529 if options.dump_json:
1530 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001531 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001532 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001533 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001534 }
maruel46b015f2015-10-13 18:40:35 -07001535 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001536 print('To collect results, use:')
1537 print(' swarming.py collect -S %s --json %s' %
1538 (options.swarming, options.dump_json))
1539 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001540 print('To collect results, use:')
1541 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001542 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1543 print('Or visit:')
1544 for t in tasks_sorted:
1545 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001546 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001547 except Failure:
1548 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001549 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001550
1551
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001552class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001553 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001554 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001555 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001556 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001557 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001558 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001559 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001560 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001561 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001562 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001563
1564 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001565 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001566 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001567 auth.process_auth_options(self, options)
1568 user = self._process_swarming(options)
1569 if hasattr(options, 'user') and not options.user:
1570 options.user = user
1571 return options, args
1572
1573 def _process_swarming(self, options):
1574 """Processes the --swarming option and aborts if not specified.
1575
1576 Returns the identity as determined by the server.
1577 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001578 if not options.swarming:
1579 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001580 try:
1581 options.swarming = net.fix_url(options.swarming)
1582 except ValueError as e:
1583 self.error('--swarming %s' % e)
1584 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001585 try:
1586 user = auth.ensure_logged_in(options.swarming)
1587 except ValueError as e:
1588 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001589 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001590
1591
1592def main(args):
1593 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001594 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001595
1596
1597if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001598 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001599 fix_encoding.fix_encoding()
1600 tools.disable_buffering()
1601 colorama.init()
1602 sys.exit(main(sys.argv[1:]))