blob: e34e091e87817df3a16ce885982cbc33336ff369 [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
43ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050044
45
46class Failure(Exception):
47 """Generic failure."""
48 pass
49
50
51### Isolated file handling.
52
53
maruel77f720b2015-09-15 12:35:22 -070054def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050055 """Archives a .isolated file if needed.
56
57 Returns the file hash to trigger and a bool specifying if it was a file (True)
58 or a hash (False).
59 """
60 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070061 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070062 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050063 if not file_hash:
64 on_error.report('Archival failure %s' % arg)
65 return None, True
66 return file_hash, True
67 elif isolated_format.is_valid_hash(arg, algo):
68 return arg, False
69 else:
70 on_error.report('Invalid hash %s' % arg)
71 return None, False
72
73
74def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050075 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050076
77 Returns:
maruel77f720b2015-09-15 12:35:22 -070078 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050079 """
80 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070081 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050082 if not options.isolated:
83 if '--' in args:
84 index = args.index('--')
85 isolated_cmd_args = args[index+1:]
86 args = args[:index]
87 else:
88 # optparse eats '--' sometimes.
89 isolated_cmd_args = args[1:]
90 args = args[:1]
91 if len(args) != 1:
92 raise ValueError(
93 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
94 'process.')
95 # Old code. To be removed eventually.
96 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070097 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050098 if not options.isolated:
99 raise ValueError('Invalid argument %s' % args[0])
100 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500101 if '--' in args:
102 index = args.index('--')
103 isolated_cmd_args = args[index+1:]
104 if index != 0:
105 raise ValueError('Unexpected arguments.')
106 else:
107 # optparse eats '--' sometimes.
108 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500109
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110 # If a file name was passed, use its base name of the isolated hash.
111 # Otherwise, use user name as an approximation of a task name.
112 if not options.task_name:
113 if is_file:
114 key = os.path.splitext(os.path.basename(args[0]))[0]
115 else:
116 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500117 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500118 key,
119 '_'.join(
120 '%s=%s' % (k, v)
121 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500122 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500123
maruel77f720b2015-09-15 12:35:22 -0700124 inputs_ref = FilesRef(
nodir152cba62016-05-12 16:08:56 -0700125 isolated=options.isolated,
126 isolatedserver=options.isolate_server,
127 namespace=options.namespace)
maruel77f720b2015-09-15 12:35:22 -0700128 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500129
130
131### Triggering.
132
133
maruel77f720b2015-09-15 12:35:22 -0700134# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -0700135CipdPackage = collections.namedtuple(
136 'CipdPackage',
137 [
138 'package_name',
139 'path',
140 'version',
141 ])
142
143
144# See ../appengine/swarming/swarming_rpcs.py.
145CipdInput = collections.namedtuple(
146 'CipdInput',
147 [
148 'client_package',
149 'packages',
150 'server',
151 ])
152
153
154# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700155FilesRef = collections.namedtuple(
156 'FilesRef',
157 [
158 'isolated',
159 'isolatedserver',
160 'namespace',
161 ])
162
163
164# See ../appengine/swarming/swarming_rpcs.py.
165TaskProperties = collections.namedtuple(
166 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500167 [
borenet02f772b2016-06-22 12:42:19 -0700168 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500169 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170 'dimensions',
171 'env',
maruel77f720b2015-09-15 12:35:22 -0700172 'execution_timeout_secs',
173 'extra_args',
174 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500175 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700176 'inputs_ref',
177 'io_timeout_secs',
178 ])
179
180
181# See ../appengine/swarming/swarming_rpcs.py.
182NewTaskRequest = collections.namedtuple(
183 'NewTaskRequest',
184 [
185 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500186 'name',
maruel77f720b2015-09-15 12:35:22 -0700187 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500188 'priority',
maruel77f720b2015-09-15 12:35:22 -0700189 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500190 'tags',
191 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500192 ])
193
194
maruel77f720b2015-09-15 12:35:22 -0700195def namedtuple_to_dict(value):
196 """Recursively converts a namedtuple to a dict."""
197 out = dict(value._asdict())
198 for k, v in out.iteritems():
199 if hasattr(v, '_asdict'):
200 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700201 elif isinstance(v, (list, tuple)):
202 l = []
203 for elem in v:
204 if hasattr(elem, '_asdict'):
205 l.append(namedtuple_to_dict(elem))
206 else:
207 l.append(elem)
208 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700209 return out
210
211
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500212def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800213 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700214
215 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500216 """
maruel77f720b2015-09-15 12:35:22 -0700217 out = namedtuple_to_dict(task_request)
218 # Maps are not supported until protobuf v3.
219 out['properties']['dimensions'] = [
220 {'key': k, 'value': v}
221 for k, v in out['properties']['dimensions'].iteritems()
222 ]
223 out['properties']['dimensions'].sort(key=lambda x: x['key'])
224 out['properties']['env'] = [
225 {'key': k, 'value': v}
226 for k, v in out['properties']['env'].iteritems()
227 ]
228 out['properties']['env'].sort(key=lambda x: x['key'])
229 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500230
231
maruel77f720b2015-09-15 12:35:22 -0700232def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500233 """Triggers a request on the Swarming server and returns the json data.
234
235 It's the low-level function.
236
237 Returns:
238 {
239 'request': {
240 'created_ts': u'2010-01-02 03:04:05',
241 'name': ..
242 },
243 'task_id': '12300',
244 }
245 """
246 logging.info('Triggering: %s', raw_request['name'])
247
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700249 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250 if not result:
251 on_error.report('Failed to trigger task %s' % raw_request['name'])
252 return None
maruele557bce2015-11-17 09:01:27 -0800253 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800254 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800255 msg = 'Failed to trigger task %s' % raw_request['name']
256 if result['error'].get('errors'):
257 for err in result['error']['errors']:
258 if err.get('message'):
259 msg += '\nMessage: %s' % err['message']
260 if err.get('debugInfo'):
261 msg += '\nDebug info:\n%s' % err['debugInfo']
262 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800263 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800264
265 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800266 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500267 return result
268
269
270def setup_googletest(env, shards, index):
271 """Sets googletest specific environment variables."""
272 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700273 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
274 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
275 env = env[:]
276 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
277 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500278 return env
279
280
281def trigger_task_shards(swarming, task_request, shards):
282 """Triggers one or many subtasks of a sharded task.
283
284 Returns:
285 Dict with task details, returned to caller as part of --dump-json output.
286 None in case of failure.
287 """
288 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700289 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500290 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700291 req['properties']['env'] = setup_googletest(
292 req['properties']['env'], shards, index)
293 req['name'] += ':%s:%s' % (index, shards)
294 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500295
296 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500297 tasks = {}
298 priority_warning = False
299 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700300 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500301 if not task:
302 break
303 logging.info('Request result: %s', task)
304 if (not priority_warning and
305 task['request']['priority'] != task_request.priority):
306 priority_warning = True
307 print >> sys.stderr, (
308 'Priority was reset to %s' % task['request']['priority'])
309 tasks[request['name']] = {
310 'shard_index': index,
311 'task_id': task['task_id'],
312 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
313 }
314
315 # Some shards weren't triggered. Abort everything.
316 if len(tasks) != len(requests):
317 if tasks:
318 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
319 len(tasks), len(requests))
320 for task_dict in tasks.itervalues():
321 abort_task(swarming, task_dict['task_id'])
322 return None
323
324 return tasks
325
326
327### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000328
329
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700330# How often to print status updates to stdout in 'collect'.
331STATUS_UPDATE_INTERVAL = 15 * 60.
332
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400333
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400334class State(object):
335 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000336
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400337 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
338 values are part of the API so if they change, the API changed.
339
340 It's in fact an enum. Values should be in decreasing order of importance.
341 """
342 RUNNING = 0x10
343 PENDING = 0x20
344 EXPIRED = 0x30
345 TIMED_OUT = 0x40
346 BOT_DIED = 0x50
347 CANCELED = 0x60
348 COMPLETED = 0x70
349
maruel77f720b2015-09-15 12:35:22 -0700350 STATES = (
351 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
352 'COMPLETED')
353 STATES_RUNNING = ('RUNNING', 'PENDING')
354 STATES_NOT_RUNNING = (
355 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
356 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
357 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400358
359 _NAMES = {
360 RUNNING: 'Running',
361 PENDING: 'Pending',
362 EXPIRED: 'Expired',
363 TIMED_OUT: 'Execution timed out',
364 BOT_DIED: 'Bot died',
365 CANCELED: 'User canceled',
366 COMPLETED: 'Completed',
367 }
368
maruel77f720b2015-09-15 12:35:22 -0700369 _ENUMS = {
370 'RUNNING': RUNNING,
371 'PENDING': PENDING,
372 'EXPIRED': EXPIRED,
373 'TIMED_OUT': TIMED_OUT,
374 'BOT_DIED': BOT_DIED,
375 'CANCELED': CANCELED,
376 'COMPLETED': COMPLETED,
377 }
378
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400379 @classmethod
380 def to_string(cls, state):
381 """Returns a user-readable string representing a State."""
382 if state not in cls._NAMES:
383 raise ValueError('Invalid state %s' % state)
384 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000385
maruel77f720b2015-09-15 12:35:22 -0700386 @classmethod
387 def from_enum(cls, state):
388 """Returns int value based on the string."""
389 if state not in cls._ENUMS:
390 raise ValueError('Invalid state %s' % state)
391 return cls._ENUMS[state]
392
maruel@chromium.org0437a732013-08-27 16:05:52 +0000393
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700394class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700395 """Assembles task execution summary (for --task-summary-json output).
396
397 Optionally fetches task outputs from isolate server to local disk (used when
398 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399
400 This object is shared among multiple threads running 'retrieve_results'
401 function, in particular they call 'process_shard_result' method in parallel.
402 """
403
maruel0eb1d1b2015-10-02 14:48:21 -0700404 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700405 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
406
407 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700408 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700409 shard_count: expected number of task shards.
410 """
maruel12e30012015-10-09 11:55:35 -0700411 self.task_output_dir = (
412 unicode(os.path.abspath(task_output_dir))
413 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700414 self.shard_count = shard_count
415
416 self._lock = threading.Lock()
417 self._per_shard_results = {}
418 self._storage = None
419
nodire5028a92016-04-29 14:38:21 -0700420 if self.task_output_dir:
421 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700422
Vadim Shtayurab450c602014-05-12 19:23:25 -0700423 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424 """Stores results of a single task shard, fetches output files if necessary.
425
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400426 Modifies |result| in place.
427
maruel77f720b2015-09-15 12:35:22 -0700428 shard_index is 0-based.
429
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430 Called concurrently from multiple threads.
431 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700432 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700433 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700434 if shard_index < 0 or shard_index >= self.shard_count:
435 logging.warning(
436 'Shard index %d is outside of expected range: [0; %d]',
437 shard_index, self.shard_count - 1)
438 return
439
maruel77f720b2015-09-15 12:35:22 -0700440 if result.get('outputs_ref'):
441 ref = result['outputs_ref']
442 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
443 ref['isolatedserver'],
444 urllib.urlencode(
445 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400446
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447 # Store result dict of that shard, ignore results we've already seen.
448 with self._lock:
449 if shard_index in self._per_shard_results:
450 logging.warning('Ignoring duplicate shard index %d', shard_index)
451 return
452 self._per_shard_results[shard_index] = result
453
454 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700455 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400456 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700457 result['outputs_ref']['isolatedserver'],
458 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400459 if storage:
460 # Output files are supposed to be small and they are not reused across
461 # tasks. So use MemoryCache for them instead of on-disk cache. Make
462 # files writable, so that calling script can delete them.
463 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700464 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400465 storage,
466 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -0700467 os.path.join(self.task_output_dir, str(shard_index)))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700468
469 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700470 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700471 with self._lock:
472 # Write an array of shard results with None for missing shards.
473 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700474 'shards': [
475 self._per_shard_results.get(i) for i in xrange(self.shard_count)
476 ],
477 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700478 # Write summary.json to task_output_dir as well.
479 if self.task_output_dir:
480 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700481 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700482 summary,
483 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700484 if self._storage:
485 self._storage.close()
486 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700487 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700488
489 def _get_storage(self, isolate_server, namespace):
490 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700491 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700492 with self._lock:
493 if not self._storage:
494 self._storage = isolateserver.get_storage(isolate_server, namespace)
495 else:
496 # Shards must all use exact same isolate server and namespace.
497 if self._storage.location != isolate_server:
498 logging.error(
499 'Task shards are using multiple isolate servers: %s and %s',
500 self._storage.location, isolate_server)
501 return None
502 if self._storage.namespace != namespace:
503 logging.error(
504 'Task shards are using multiple namespaces: %s and %s',
505 self._storage.namespace, namespace)
506 return None
507 return self._storage
508
509
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500510def now():
511 """Exists so it can be mocked easily."""
512 return time.time()
513
514
maruel77f720b2015-09-15 12:35:22 -0700515def parse_time(value):
516 """Converts serialized time from the API to datetime.datetime."""
517 # When microseconds are 0, the '.123456' suffix is elided. This means the
518 # serialized format is not consistent, which confuses the hell out of python.
519 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
520 try:
521 return datetime.datetime.strptime(value, fmt)
522 except ValueError:
523 pass
524 raise ValueError('Failed to parse %s' % value)
525
526
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700527def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700528 base_url, shard_index, task_id, timeout, should_stop, output_collector,
529 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400530 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700531
Vadim Shtayurab450c602014-05-12 19:23:25 -0700532 Returns:
533 <result dict> on success.
534 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700535 """
maruel71c61c82016-02-22 06:52:05 -0800536 assert timeout is None or isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700537 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700538 if include_perf:
539 result_url += '?include_performance_stats=true'
maruel77f720b2015-09-15 12:35:22 -0700540 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700541 started = now()
542 deadline = started + timeout if timeout else None
543 attempt = 0
544
545 while not should_stop.is_set():
546 attempt += 1
547
548 # Waiting for too long -> give up.
549 current_time = now()
550 if deadline and current_time >= deadline:
551 logging.error('retrieve_results(%s) timed out on attempt %d',
552 base_url, attempt)
553 return None
554
555 # Do not spin too fast. Spin faster at the beginning though.
556 # Start with 1 sec delay and for each 30 sec of waiting add another second
557 # of delay, until hitting 15 sec ceiling.
558 if attempt > 1:
559 max_delay = min(15, 1 + (current_time - started) / 30.0)
560 delay = min(max_delay, deadline - current_time) if deadline else max_delay
561 if delay > 0:
562 logging.debug('Waiting %.1f sec before retrying', delay)
563 should_stop.wait(delay)
564 if should_stop.is_set():
565 return None
566
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400567 # Disable internal retries in net.url_read_json, since we are doing retries
568 # ourselves.
569 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700570 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
571 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400572 result = net.url_read_json(result_url, retry_50x=False)
573 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400574 continue
maruel77f720b2015-09-15 12:35:22 -0700575
maruelbf53e042015-12-01 15:00:51 -0800576 if result.get('error'):
577 # An error occurred.
578 if result['error'].get('errors'):
579 for err in result['error']['errors']:
580 logging.warning(
581 'Error while reading task: %s; %s',
582 err.get('message'), err.get('debugInfo'))
583 elif result['error'].get('message'):
584 logging.warning(
585 'Error while reading task: %s', result['error']['message'])
586 continue
587
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400588 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700589 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400590 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700591 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700592 # Record the result, try to fetch attached output files (if any).
593 if output_collector:
594 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700595 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700596 if result.get('internal_failure'):
597 logging.error('Internal error!')
598 elif result['state'] == 'BOT_DIED':
599 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700600 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000601
602
maruel77f720b2015-09-15 12:35:22 -0700603def convert_to_old_format(result):
604 """Converts the task result data from Endpoints API format to old API format
605 for compatibility.
606
607 This goes into the file generated as --task-summary-json.
608 """
609 # Sets default.
610 result.setdefault('abandoned_ts', None)
611 result.setdefault('bot_id', None)
612 result.setdefault('bot_version', None)
613 result.setdefault('children_task_ids', [])
614 result.setdefault('completed_ts', None)
615 result.setdefault('cost_saved_usd', None)
616 result.setdefault('costs_usd', None)
617 result.setdefault('deduped_from', None)
618 result.setdefault('name', None)
619 result.setdefault('outputs_ref', None)
620 result.setdefault('properties_hash', None)
621 result.setdefault('server_versions', None)
622 result.setdefault('started_ts', None)
623 result.setdefault('tags', None)
624 result.setdefault('user', None)
625
626 # Convertion back to old API.
627 duration = result.pop('duration', None)
628 result['durations'] = [duration] if duration else []
629 exit_code = result.pop('exit_code', None)
630 result['exit_codes'] = [int(exit_code)] if exit_code else []
631 result['id'] = result.pop('task_id')
632 result['isolated_out'] = result.get('outputs_ref', None)
633 output = result.pop('output', None)
634 result['outputs'] = [output] if output else []
635 # properties_hash
636 # server_version
637 # Endpoints result 'state' as string. For compatibility with old code, convert
638 # to int.
639 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700640 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700641 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700642 if 'bot_dimensions' in result:
643 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700644 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700645 }
646 else:
647 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700648
649
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700650def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400651 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700652 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500653 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000654
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700655 Duplicate shards are ignored. Shards are yielded in order of completion.
656 Timed out shards are NOT yielded at all. Caller can compare number of yielded
657 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000658
659 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500660 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 +0000661 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500662
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700663 output_collector is an optional instance of TaskOutputCollector that will be
664 used to fetch files produced by a task from isolate server to the local disk.
665
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500666 Yields:
667 (index, result). In particular, 'result' is defined as the
668 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000669 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000670 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400671 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700672 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700673 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700674
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
676 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700677 # Adds a task to the thread pool to call 'retrieve_results' and return
678 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400679 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700680 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400682 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700683 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700684
685 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400686 for shard_index, task_id in enumerate(task_ids):
687 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700688
689 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400690 shards_remaining = range(len(task_ids))
691 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700692 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700693 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700694 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700695 shard_index, result = results_channel.pull(
696 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700697 except threading_utils.TaskChannel.Timeout:
698 if print_status_updates:
699 print(
700 'Waiting for results from the following shards: %s' %
701 ', '.join(map(str, shards_remaining)))
702 sys.stdout.flush()
703 continue
704 except Exception:
705 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700706
707 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700708 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000709 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500710 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000711 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700712
Vadim Shtayurab450c602014-05-12 19:23:25 -0700713 # Yield back results to the caller.
714 assert shard_index in shards_remaining
715 shards_remaining.remove(shard_index)
716 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700717
maruel@chromium.org0437a732013-08-27 16:05:52 +0000718 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700719 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000720 should_stop.set()
721
722
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400723def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000724 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700725 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400726 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700727 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
728 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400729 else:
730 pending = 'N/A'
731
maruel77f720b2015-09-15 12:35:22 -0700732 if metadata.get('duration') is not None:
733 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400734 else:
735 duration = 'N/A'
736
maruel77f720b2015-09-15 12:35:22 -0700737 if metadata.get('exit_code') is not None:
738 # Integers are encoded as string to not loose precision.
739 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400740 else:
741 exit_code = 'N/A'
742
743 bot_id = metadata.get('bot_id') or 'N/A'
744
maruel77f720b2015-09-15 12:35:22 -0700745 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400746 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400747 tag_footer = (
748 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
749 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400750
751 tag_len = max(len(tag_header), len(tag_footer))
752 dash_pad = '+-%s-+\n' % ('-' * tag_len)
753 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
754 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
755
756 header = dash_pad + tag_header + dash_pad
757 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700758 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400759 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000760
761
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700762def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700763 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700764 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700765 """Retrieves results of a Swarming task.
766
767 Returns:
768 process exit code that should be returned to the user.
769 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700770 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700771 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700772
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700773 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700774 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400775 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700776 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400777 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400778 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700779 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700780 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700781
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400782 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700783 shard_exit_code = metadata.get('exit_code')
784 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700785 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700786 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700787 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400788 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700789 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700790
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700791 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400792 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400793 if len(seen_shards) < len(task_ids):
794 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700795 else:
maruel77f720b2015-09-15 12:35:22 -0700796 print('%s: %s %s' % (
797 metadata.get('bot_id', 'N/A'),
798 metadata['task_id'],
799 shard_exit_code))
800 if metadata['output']:
801 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400802 if output:
803 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700804 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700805 summary = output_collector.finalize()
806 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700807 # TODO(maruel): Make this optional.
808 for i in summary['shards']:
809 if i:
810 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700811 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700812
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400813 if decorate and total_duration:
814 print('Total duration: %.1fs' % total_duration)
815
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400816 if len(seen_shards) != len(task_ids):
817 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700818 print >> sys.stderr, ('Results from some shards are missing: %s' %
819 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700820 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700821
maruela5490782015-09-30 10:56:59 -0700822 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000823
824
maruel77f720b2015-09-15 12:35:22 -0700825### API management.
826
827
828class APIError(Exception):
829 pass
830
831
832def endpoints_api_discovery_apis(host):
833 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
834 the APIs exposed by a host.
835
836 https://developers.google.com/discovery/v1/reference/apis/list
837 """
838 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
839 if data is None:
840 raise APIError('Failed to discover APIs on %s' % host)
841 out = {}
842 for api in data['items']:
843 if api['id'] == 'discovery:v1':
844 continue
845 # URL is of the following form:
846 # url = host + (
847 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
848 api_data = net.url_read_json(api['discoveryRestUrl'])
849 if api_data is None:
850 raise APIError('Failed to discover %s on %s' % (api['id'], host))
851 out[api['id']] = api_data
852 return out
853
854
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500855### Commands.
856
857
858def abort_task(_swarming, _manifest):
859 """Given a task manifest that was triggered, aborts its execution."""
860 # TODO(vadimsh): No supported by the server yet.
861
862
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400863def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400864 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500865 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500866 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500867 dest='dimensions', metavar='FOO bar',
868 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500869 parser.add_option_group(parser.filter_group)
870
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400871
Vadim Shtayurab450c602014-05-12 19:23:25 -0700872def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400873 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700874 parser.sharding_group.add_option(
875 '--shards', type='int', default=1,
876 help='Number of shards to trigger and collect.')
877 parser.add_option_group(parser.sharding_group)
878
879
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400880def add_trigger_options(parser):
881 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500882 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400883 add_filter_options(parser)
884
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400885 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500886 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500887 '-s', '--isolated',
888 help='Hash of the .isolated to grab from the isolate server')
889 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500890 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700891 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500892 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500893 '--priority', type='int', default=100,
894 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500895 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500896 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400897 help='Display name of the task. Defaults to '
898 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
899 'isolated file is provided, if a hash is provided, it defaults to '
900 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400901 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400902 '--tags', action='append', default=[],
903 help='Tags to assign to the task.')
904 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500905 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400906 help='User associated with the task. Defaults to authenticated user on '
907 'the server.')
908 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400909 '--idempotent', action='store_true', default=False,
910 help='When set, the server will actively try to find a previous task '
911 'with the same parameter and return this result instead if possible')
912 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400913 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400914 help='Seconds to allow the task to be pending for a bot to run before '
915 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400916 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400917 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400918 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400919 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400920 '--hard-timeout', type='int', default=60*60,
921 help='Seconds to allow the task to complete.')
922 parser.task_group.add_option(
923 '--io-timeout', type='int', default=20*60,
924 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500925 parser.task_group.add_option(
926 '--raw-cmd', action='store_true', default=False,
927 help='When set, the command after -- is used as-is without run_isolated. '
928 'In this case, no .isolated file is expected.')
borenet02f772b2016-06-22 12:42:19 -0700929 parser.task_group.add_option(
930 '--cipd-package', action='append', default=[],
931 help='CIPD packages to install on the Swarming bot. Uses the format: '
932 'path:package_name:version')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500933 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000934
935
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500936def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500937 """Processes trigger options and uploads files to isolate server if necessary.
938 """
939 options.dimensions = dict(options.dimensions)
940 options.env = dict(options.env)
941
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500942 if not options.dimensions:
943 parser.error('Please at least specify one --dimension')
944 if options.raw_cmd:
945 if not args:
946 parser.error(
947 'Arguments with --raw-cmd should be passed after -- as command '
948 'delimiter.')
949 if options.isolate_server:
950 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
951
952 command = args
953 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500954 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500955 options.user,
956 '_'.join(
957 '%s=%s' % (k, v)
958 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700959 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500960 else:
nodir55be77b2016-05-03 09:39:57 -0700961 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500962 try:
maruel77f720b2015-09-15 12:35:22 -0700963 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500964 except ValueError as e:
965 parser.error(str(e))
966
borenet02f772b2016-06-22 12:42:19 -0700967 cipd_packages = []
968 for p in options.cipd_package:
969 split = p.split(':', 2)
970 if len(split) != 3:
971 parser.error('CIPD packages must take the form: path:package:version')
972 cipd_packages.append(CipdPackage(
973 package_name=split[1],
974 path=split[0],
975 version=split[2]))
976 cipd_input = None
977 if cipd_packages:
978 cipd_input = CipdInput(
979 client_package=None,
980 packages=cipd_packages,
981 server=None)
982
nodir152cba62016-05-12 16:08:56 -0700983 # If inputs_ref.isolated is used, command is actually extra_args.
984 # Otherwise it's an actual command to run.
985 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -0700986 properties = TaskProperties(
borenet02f772b2016-06-22 12:42:19 -0700987 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -0700988 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500989 dimensions=options.dimensions,
990 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700991 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -0700992 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -0700993 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500994 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700995 inputs_ref=inputs_ref,
996 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700997 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
998 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700999 return NewTaskRequest(
1000 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001001 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001002 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001003 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001004 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001005 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001006 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001007
1008
1009def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001010 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001011 '-t', '--timeout', type='float',
1012 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1013 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001014 parser.group_logging.add_option(
1015 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001016 parser.group_logging.add_option(
1017 '--print-status-updates', action='store_true',
1018 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001019 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001020 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001021 '--task-summary-json',
1022 metavar='FILE',
1023 help='Dump a summary of task results to this file as json. It contains '
1024 'only shards statuses as know to server directly. Any output files '
1025 'emitted by the task can be collected by using --task-output-dir')
1026 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001027 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001028 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001029 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001030 'directory contains per-shard directory with output files produced '
1031 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001032 parser.task_output_group.add_option(
1033 '--perf', action='store_true', default=False,
1034 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001035 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001036
1037
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001038@subcommand.usage('bots...')
1039def CMDbot_delete(parser, args):
1040 """Forcibly deletes bots from the Swarming server."""
1041 parser.add_option(
1042 '-f', '--force', action='store_true',
1043 help='Do not prompt for confirmation')
1044 options, args = parser.parse_args(args)
1045 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001046 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001047
1048 bots = sorted(args)
1049 if not options.force:
1050 print('Delete the following bots?')
1051 for bot in bots:
1052 print(' %s' % bot)
1053 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1054 print('Goodbye.')
1055 return 1
1056
1057 result = 0
1058 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001059 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1060 if net.url_read_json(url, data={}, method='POST') is None:
1061 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001062 result = 1
1063 return result
1064
1065
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001066def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001067 """Returns information about the bots connected to the Swarming server."""
1068 add_filter_options(parser)
1069 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001070 '--dead-only', action='store_true',
1071 help='Only print dead bots, useful to reap them and reimage broken bots')
1072 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001073 '-k', '--keep-dead', action='store_true',
1074 help='Do not filter out dead bots')
1075 parser.filter_group.add_option(
1076 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001077 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001078 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001079
1080 if options.keep_dead and options.dead_only:
1081 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001082
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001083 bots = []
1084 cursor = None
1085 limit = 250
1086 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001087 base_url = (
1088 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001089 while True:
1090 url = base_url
1091 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001092 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001093 data = net.url_read_json(url)
1094 if data is None:
1095 print >> sys.stderr, 'Failed to access %s' % options.swarming
1096 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001097 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001098 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001099 if not cursor:
1100 break
1101
maruel77f720b2015-09-15 12:35:22 -07001102 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001103 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001104 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001105 continue
maruel77f720b2015-09-15 12:35:22 -07001106 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001107 continue
1108
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001109 # If the user requested to filter on dimensions, ensure the bot has all the
1110 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001111 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001112 for key, value in options.dimensions:
1113 if key not in dimensions:
1114 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001115 # A bot can have multiple value for a key, for example,
1116 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1117 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001118 if isinstance(dimensions[key], list):
1119 if value not in dimensions[key]:
1120 break
1121 else:
1122 if value != dimensions[key]:
1123 break
1124 else:
maruel77f720b2015-09-15 12:35:22 -07001125 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001126 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001127 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001128 if bot.get('task_id'):
1129 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001130 return 0
1131
1132
maruelfd0a90c2016-06-10 11:51:10 -07001133@subcommand.usage('task_id')
1134def CMDcancel(parser, args):
1135 """Cancels a task."""
1136 options, args = parser.parse_args(args)
1137 if not args:
1138 parser.error('Please specify the task to cancel')
1139 for task_id in args:
1140 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
1141 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1142 print('Deleting %s failed. Probably already gone' % task_id)
1143 return 1
1144 return 0
1145
1146
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001147@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001148def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001149 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001150
1151 The result can be in multiple part if the execution was sharded. It can
1152 potentially have retries.
1153 """
1154 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001155 parser.add_option(
1156 '-j', '--json',
1157 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001158 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001159 if not args and not options.json:
1160 parser.error('Must specify at least one task id or --json.')
1161 if args and options.json:
1162 parser.error('Only use one of task id or --json.')
1163
1164 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001165 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001166 try:
maruel1ceb3872015-10-14 06:10:44 -07001167 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001168 data = json.load(f)
1169 except (IOError, ValueError):
1170 parser.error('Failed to open %s' % options.json)
1171 try:
1172 tasks = sorted(
1173 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1174 args = [t['task_id'] for t in tasks]
1175 except (KeyError, TypeError):
1176 parser.error('Failed to process %s' % options.json)
1177 if options.timeout is None:
1178 options.timeout = (
1179 data['request']['properties']['execution_timeout_secs'] +
1180 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001181 else:
1182 valid = frozenset('0123456789abcdef')
1183 if any(not valid.issuperset(task_id) for task_id in args):
1184 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001185
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001186 try:
1187 return collect(
1188 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001189 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001190 options.timeout,
1191 options.decorate,
1192 options.print_status_updates,
1193 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001194 options.task_output_dir,
1195 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001196 except Failure:
1197 on_error.report(None)
1198 return 1
1199
1200
maruelbea00862015-09-18 09:55:36 -07001201@subcommand.usage('[filename]')
1202def CMDput_bootstrap(parser, args):
1203 """Uploads a new version of bootstrap.py."""
1204 options, args = parser.parse_args(args)
1205 if len(args) != 1:
1206 parser.error('Must specify file to upload')
1207 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001208 path = unicode(os.path.abspath(args[0]))
1209 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001210 content = f.read().decode('utf-8')
1211 data = net.url_read_json(url, data={'content': content})
1212 print data
1213 return 0
1214
1215
1216@subcommand.usage('[filename]')
1217def CMDput_bot_config(parser, args):
1218 """Uploads a new version of bot_config.py."""
1219 options, args = parser.parse_args(args)
1220 if len(args) != 1:
1221 parser.error('Must specify file to upload')
1222 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001223 path = unicode(os.path.abspath(args[0]))
1224 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001225 content = f.read().decode('utf-8')
1226 data = net.url_read_json(url, data={'content': content})
1227 print data
1228 return 0
1229
1230
maruel77f720b2015-09-15 12:35:22 -07001231@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001232def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001233 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1234 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001235
1236 Examples:
maruel77f720b2015-09-15 12:35:22 -07001237 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001238 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001239
maruel77f720b2015-09-15 12:35:22 -07001240 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001241 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1242
1243 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1244 quoting is important!:
1245 swarming.py query -S server-url.com --limit 10 \\
1246 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001247 """
1248 CHUNK_SIZE = 250
1249
1250 parser.add_option(
1251 '-L', '--limit', type='int', default=200,
1252 help='Limit to enforce on limitless items (like number of tasks); '
1253 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001254 parser.add_option(
1255 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001256 parser.add_option(
1257 '--progress', action='store_true',
1258 help='Prints a dot at each request to show progress')
1259 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001260 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001261 parser.error(
1262 'Must specify only method name and optionally query args properly '
1263 'escaped.')
1264 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001265 url = base_url
1266 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001267 # Check check, change if not working out.
1268 merge_char = '&' if '?' in url else '?'
1269 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001270 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001271 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001272 # TODO(maruel): Do basic diagnostic.
1273 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001274 return 1
1275
1276 # Some items support cursors. Try to get automatically if cursors are needed
1277 # by looking at the 'cursor' items.
1278 while (
1279 data.get('cursor') and
1280 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001281 merge_char = '&' if '?' in base_url else '?'
1282 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001283 if options.limit:
1284 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001285 if options.progress:
1286 sys.stdout.write('.')
1287 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001288 new = net.url_read_json(url)
1289 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001290 if options.progress:
1291 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001292 print >> sys.stderr, 'Failed to access %s' % options.swarming
1293 return 1
maruel81b37132015-10-21 06:42:13 -07001294 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001295 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001296
maruel77f720b2015-09-15 12:35:22 -07001297 if options.progress:
1298 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001299 if options.limit and len(data.get('items', [])) > options.limit:
1300 data['items'] = data['items'][:options.limit]
1301 data.pop('cursor', None)
1302
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001303 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001304 options.json = unicode(os.path.abspath(options.json))
1305 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001306 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001307 try:
maruel77f720b2015-09-15 12:35:22 -07001308 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001309 sys.stdout.write('\n')
1310 except IOError:
1311 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001312 return 0
1313
1314
maruel77f720b2015-09-15 12:35:22 -07001315def CMDquery_list(parser, args):
1316 """Returns list of all the Swarming APIs that can be used with command
1317 'query'.
1318 """
1319 parser.add_option(
1320 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1321 options, args = parser.parse_args(args)
1322 if args:
1323 parser.error('No argument allowed.')
1324
1325 try:
1326 apis = endpoints_api_discovery_apis(options.swarming)
1327 except APIError as e:
1328 parser.error(str(e))
1329 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001330 options.json = unicode(os.path.abspath(options.json))
1331 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001332 json.dump(apis, f)
1333 else:
1334 help_url = (
1335 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1336 options.swarming)
1337 for api_id, api in sorted(apis.iteritems()):
1338 print api_id
1339 print ' ' + api['description']
1340 for resource_name, resource in sorted(api['resources'].iteritems()):
1341 print ''
1342 for method_name, method in sorted(resource['methods'].iteritems()):
1343 # Only list the GET ones.
1344 if method['httpMethod'] != 'GET':
1345 continue
1346 print '- %s.%s: %s' % (
1347 resource_name, method_name, method['path'])
1348 print ' ' + method['description']
1349 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1350 return 0
1351
1352
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001353@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001354def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001355 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001356
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001357 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001358 """
1359 add_trigger_options(parser)
1360 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001361 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001362 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001363 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001364 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001365 tasks = trigger_task_shards(
1366 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001367 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001368 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001369 'Failed to trigger %s(%s): %s' %
1370 (options.task_name, args[0], e.args[0]))
1371 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001372 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001373 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001374 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001375 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001376 task_ids = [
1377 t['task_id']
1378 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1379 ]
maruel71c61c82016-02-22 06:52:05 -08001380 if options.timeout is None:
1381 options.timeout = (
1382 task_request.properties.execution_timeout_secs +
1383 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001384 try:
1385 return collect(
1386 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001387 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001388 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001389 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001390 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001391 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001392 options.task_output_dir,
1393 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001394 except Failure:
1395 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001396 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001397
1398
maruel18122c62015-10-23 06:31:23 -07001399@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001400def CMDreproduce(parser, args):
1401 """Runs a task locally that was triggered on the server.
1402
1403 This running locally the same commands that have been run on the bot. The data
1404 downloaded will be in a subdirectory named 'work' of the current working
1405 directory.
maruel18122c62015-10-23 06:31:23 -07001406
1407 You can pass further additional arguments to the target command by passing
1408 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001409 """
maruelc070e672016-02-22 17:32:57 -08001410 parser.add_option(
1411 '--output-dir', metavar='DIR', default='',
1412 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001413 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001414 extra_args = []
1415 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001416 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001417 if len(args) > 1:
1418 if args[1] == '--':
1419 if len(args) > 2:
1420 extra_args = args[2:]
1421 else:
1422 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001423
maruel77f720b2015-09-15 12:35:22 -07001424 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001425 request = net.url_read_json(url)
1426 if not request:
1427 print >> sys.stderr, 'Failed to retrieve request data for the task'
1428 return 1
1429
maruel12e30012015-10-09 11:55:35 -07001430 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001431 if fs.isdir(workdir):
1432 parser.error('Please delete the directory \'work\' first')
1433 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001434
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001435 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001436 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001437 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001438 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001439 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001440 for i in properties['env']:
1441 key = i['key'].encode('utf-8')
1442 if not i['value']:
1443 env.pop(key, None)
1444 else:
1445 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001446
nodir152cba62016-05-12 16:08:56 -07001447 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001448 # Create the tree.
1449 with isolateserver.get_storage(
1450 properties['inputs_ref']['isolatedserver'],
1451 properties['inputs_ref']['namespace']) as storage:
1452 bundle = isolateserver.fetch_isolated(
1453 properties['inputs_ref']['isolated'],
1454 storage,
1455 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -07001456 workdir)
maruel29ab2fd2015-10-16 11:44:01 -07001457 command = bundle.command
1458 if bundle.relative_cwd:
1459 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001460 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001461 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001462 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001463 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001464 if not options.output_dir and new_command != command:
1465 parser.error('The task has outputs, you must use --output-dir')
1466 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001467 else:
1468 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001469 try:
maruel18122c62015-10-23 06:31:23 -07001470 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001471 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001472 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001473 print >> sys.stderr, str(e)
1474 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001475
1476
maruel0eb1d1b2015-10-02 14:48:21 -07001477@subcommand.usage('bot_id')
1478def CMDterminate(parser, args):
1479 """Tells a bot to gracefully shut itself down as soon as it can.
1480
1481 This is done by completing whatever current task there is then exiting the bot
1482 process.
1483 """
1484 parser.add_option(
1485 '--wait', action='store_true', help='Wait for the bot to terminate')
1486 options, args = parser.parse_args(args)
1487 if len(args) != 1:
1488 parser.error('Please provide the bot id')
1489 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1490 request = net.url_read_json(url, data={})
1491 if not request:
1492 print >> sys.stderr, 'Failed to ask for termination'
1493 return 1
1494 if options.wait:
1495 return collect(
maruel9531ce02016-04-13 06:11:23 -07001496 options.swarming, [request['task_id']], 0., False, False, None, None,
1497 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001498 return 0
1499
1500
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001501@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001502def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001503 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001504
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001505 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001506 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001507
1508 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001509
1510 Passes all extra arguments provided after '--' as additional command line
1511 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001512 """
1513 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001514 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001515 parser.add_option(
1516 '--dump-json',
1517 metavar='FILE',
1518 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001519 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001520 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001521 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001522 tasks = trigger_task_shards(
1523 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001524 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001525 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001526 tasks_sorted = sorted(
1527 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001528 if options.dump_json:
1529 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001530 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001531 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001532 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001533 }
maruel46b015f2015-10-13 18:40:35 -07001534 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001535 print('To collect results, use:')
1536 print(' swarming.py collect -S %s --json %s' %
1537 (options.swarming, options.dump_json))
1538 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001539 print('To collect results, use:')
1540 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001541 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1542 print('Or visit:')
1543 for t in tasks_sorted:
1544 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001545 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001546 except Failure:
1547 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001548 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001549
1550
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001551class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001552 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001553 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001554 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001555 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001556 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001557 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001558 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001559 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001560 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001561 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001562
1563 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001564 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001565 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001566 auth.process_auth_options(self, options)
1567 user = self._process_swarming(options)
1568 if hasattr(options, 'user') and not options.user:
1569 options.user = user
1570 return options, args
1571
1572 def _process_swarming(self, options):
1573 """Processes the --swarming option and aborts if not specified.
1574
1575 Returns the identity as determined by the server.
1576 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001577 if not options.swarming:
1578 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001579 try:
1580 options.swarming = net.fix_url(options.swarming)
1581 except ValueError as e:
1582 self.error('--swarming %s' % e)
1583 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001584 try:
1585 user = auth.ensure_logged_in(options.swarming)
1586 except ValueError as e:
1587 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001588 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001589
1590
1591def main(args):
1592 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001593 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001594
1595
1596if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001597 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001598 fix_encoding.fix_encoding()
1599 tools.disable_buffering()
1600 colorama.init()
1601 sys.exit(main(sys.argv[1:]))