blob: b06dfa483eabfbf4340a1156591d24330a5b12ad [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.
3# Use of this source code is governed by the Apache v2.0 license that can be
4# 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
maruelb76604c2015-11-11 11:53:44 -08008__version__ = '0.8.4'
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
maruel@chromium.org0437a732013-08-27 16:05:52 +000033from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000034from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000035
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040037import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000038import isolateserver
maruelc070e672016-02-22 17:32:57 -080039import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000040
41
42ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050043
44
45class Failure(Exception):
46 """Generic failure."""
47 pass
48
49
50### Isolated file handling.
51
52
maruel77f720b2015-09-15 12:35:22 -070053def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050054 """Archives a .isolated file if needed.
55
56 Returns the file hash to trigger and a bool specifying if it was a file (True)
57 or a hash (False).
58 """
59 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070060 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070061 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050062 if not file_hash:
63 on_error.report('Archival failure %s' % arg)
64 return None, True
65 return file_hash, True
66 elif isolated_format.is_valid_hash(arg, algo):
67 return arg, False
68 else:
69 on_error.report('Invalid hash %s' % arg)
70 return None, False
71
72
73def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050074 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050075
76 Returns:
maruel77f720b2015-09-15 12:35:22 -070077 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050078 """
79 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070080 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050081 if not options.isolated:
82 if '--' in args:
83 index = args.index('--')
84 isolated_cmd_args = args[index+1:]
85 args = args[:index]
86 else:
87 # optparse eats '--' sometimes.
88 isolated_cmd_args = args[1:]
89 args = args[:1]
90 if len(args) != 1:
91 raise ValueError(
92 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
93 'process.')
94 # Old code. To be removed eventually.
95 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070096 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050097 if not options.isolated:
98 raise ValueError('Invalid argument %s' % args[0])
99 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500100 if '--' in args:
101 index = args.index('--')
102 isolated_cmd_args = args[index+1:]
103 if index != 0:
104 raise ValueError('Unexpected arguments.')
105 else:
106 # optparse eats '--' sometimes.
107 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500108
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500109 # If a file name was passed, use its base name of the isolated hash.
110 # Otherwise, use user name as an approximation of a task name.
111 if not options.task_name:
112 if is_file:
113 key = os.path.splitext(os.path.basename(args[0]))[0]
114 else:
115 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500116 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500117 key,
118 '_'.join(
119 '%s=%s' % (k, v)
120 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500121 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500122
maruel77f720b2015-09-15 12:35:22 -0700123 inputs_ref = FilesRef(
124 isolated=options.isolated,
125 isolatedserver=options.isolate_server,
126 namespace=options.namespace)
127 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500128
129
130### Triggering.
131
132
maruel77f720b2015-09-15 12:35:22 -0700133# See ../appengine/swarming/swarming_rpcs.py.
134FilesRef = collections.namedtuple(
135 'FilesRef',
136 [
137 'isolated',
138 'isolatedserver',
139 'namespace',
140 ])
141
142
143# See ../appengine/swarming/swarming_rpcs.py.
144TaskProperties = collections.namedtuple(
145 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500146 [
147 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500148 'dimensions',
149 'env',
maruel77f720b2015-09-15 12:35:22 -0700150 'execution_timeout_secs',
151 'extra_args',
152 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500153 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700154 'inputs_ref',
155 'io_timeout_secs',
156 ])
157
158
159# See ../appengine/swarming/swarming_rpcs.py.
160NewTaskRequest = collections.namedtuple(
161 'NewTaskRequest',
162 [
163 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 'name',
maruel77f720b2015-09-15 12:35:22 -0700165 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500166 'priority',
maruel77f720b2015-09-15 12:35:22 -0700167 'properties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500168 'tags',
169 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170 ])
171
172
maruel77f720b2015-09-15 12:35:22 -0700173def namedtuple_to_dict(value):
174 """Recursively converts a namedtuple to a dict."""
175 out = dict(value._asdict())
176 for k, v in out.iteritems():
177 if hasattr(v, '_asdict'):
178 out[k] = namedtuple_to_dict(v)
179 return out
180
181
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500182def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800183 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700184
185 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500186 """
maruel77f720b2015-09-15 12:35:22 -0700187 out = namedtuple_to_dict(task_request)
188 # Maps are not supported until protobuf v3.
189 out['properties']['dimensions'] = [
190 {'key': k, 'value': v}
191 for k, v in out['properties']['dimensions'].iteritems()
192 ]
193 out['properties']['dimensions'].sort(key=lambda x: x['key'])
194 out['properties']['env'] = [
195 {'key': k, 'value': v}
196 for k, v in out['properties']['env'].iteritems()
197 ]
198 out['properties']['env'].sort(key=lambda x: x['key'])
199 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500200
201
maruel77f720b2015-09-15 12:35:22 -0700202def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500203 """Triggers a request on the Swarming server and returns the json data.
204
205 It's the low-level function.
206
207 Returns:
208 {
209 'request': {
210 'created_ts': u'2010-01-02 03:04:05',
211 'name': ..
212 },
213 'task_id': '12300',
214 }
215 """
216 logging.info('Triggering: %s', raw_request['name'])
217
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500218 result = net.url_read_json(
maruel77f720b2015-09-15 12:35:22 -0700219 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500220 if not result:
221 on_error.report('Failed to trigger task %s' % raw_request['name'])
222 return None
maruele557bce2015-11-17 09:01:27 -0800223 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800224 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800225 msg = 'Failed to trigger task %s' % raw_request['name']
226 if result['error'].get('errors'):
227 for err in result['error']['errors']:
228 if err.get('message'):
229 msg += '\nMessage: %s' % err['message']
230 if err.get('debugInfo'):
231 msg += '\nDebug info:\n%s' % err['debugInfo']
232 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800233 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800234
235 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800236 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500237 return result
238
239
240def setup_googletest(env, shards, index):
241 """Sets googletest specific environment variables."""
242 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700243 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
244 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
245 env = env[:]
246 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
247 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 return env
249
250
251def trigger_task_shards(swarming, task_request, shards):
252 """Triggers one or many subtasks of a sharded task.
253
254 Returns:
255 Dict with task details, returned to caller as part of --dump-json output.
256 None in case of failure.
257 """
258 def convert(index):
maruel77f720b2015-09-15 12:35:22 -0700259 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500260 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700261 req['properties']['env'] = setup_googletest(
262 req['properties']['env'], shards, index)
263 req['name'] += ':%s:%s' % (index, shards)
264 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500265
266 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500267 tasks = {}
268 priority_warning = False
269 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700270 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500271 if not task:
272 break
273 logging.info('Request result: %s', task)
274 if (not priority_warning and
275 task['request']['priority'] != task_request.priority):
276 priority_warning = True
277 print >> sys.stderr, (
278 'Priority was reset to %s' % task['request']['priority'])
279 tasks[request['name']] = {
280 'shard_index': index,
281 'task_id': task['task_id'],
282 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
283 }
284
285 # Some shards weren't triggered. Abort everything.
286 if len(tasks) != len(requests):
287 if tasks:
288 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
289 len(tasks), len(requests))
290 for task_dict in tasks.itervalues():
291 abort_task(swarming, task_dict['task_id'])
292 return None
293
294 return tasks
295
296
297### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000298
299
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700300# How often to print status updates to stdout in 'collect'.
301STATUS_UPDATE_INTERVAL = 15 * 60.
302
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400303
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400304class State(object):
305 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000306
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400307 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
308 values are part of the API so if they change, the API changed.
309
310 It's in fact an enum. Values should be in decreasing order of importance.
311 """
312 RUNNING = 0x10
313 PENDING = 0x20
314 EXPIRED = 0x30
315 TIMED_OUT = 0x40
316 BOT_DIED = 0x50
317 CANCELED = 0x60
318 COMPLETED = 0x70
319
maruel77f720b2015-09-15 12:35:22 -0700320 STATES = (
321 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
322 'COMPLETED')
323 STATES_RUNNING = ('RUNNING', 'PENDING')
324 STATES_NOT_RUNNING = (
325 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
326 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
327 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400328
329 _NAMES = {
330 RUNNING: 'Running',
331 PENDING: 'Pending',
332 EXPIRED: 'Expired',
333 TIMED_OUT: 'Execution timed out',
334 BOT_DIED: 'Bot died',
335 CANCELED: 'User canceled',
336 COMPLETED: 'Completed',
337 }
338
maruel77f720b2015-09-15 12:35:22 -0700339 _ENUMS = {
340 'RUNNING': RUNNING,
341 'PENDING': PENDING,
342 'EXPIRED': EXPIRED,
343 'TIMED_OUT': TIMED_OUT,
344 'BOT_DIED': BOT_DIED,
345 'CANCELED': CANCELED,
346 'COMPLETED': COMPLETED,
347 }
348
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400349 @classmethod
350 def to_string(cls, state):
351 """Returns a user-readable string representing a State."""
352 if state not in cls._NAMES:
353 raise ValueError('Invalid state %s' % state)
354 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000355
maruel77f720b2015-09-15 12:35:22 -0700356 @classmethod
357 def from_enum(cls, state):
358 """Returns int value based on the string."""
359 if state not in cls._ENUMS:
360 raise ValueError('Invalid state %s' % state)
361 return cls._ENUMS[state]
362
maruel@chromium.org0437a732013-08-27 16:05:52 +0000363
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700365 """Assembles task execution summary (for --task-summary-json output).
366
367 Optionally fetches task outputs from isolate server to local disk (used when
368 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369
370 This object is shared among multiple threads running 'retrieve_results'
371 function, in particular they call 'process_shard_result' method in parallel.
372 """
373
maruel0eb1d1b2015-10-02 14:48:21 -0700374 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700375 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
376
377 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700378 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700379 shard_count: expected number of task shards.
380 """
maruel12e30012015-10-09 11:55:35 -0700381 self.task_output_dir = (
382 unicode(os.path.abspath(task_output_dir))
383 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 self.shard_count = shard_count
385
386 self._lock = threading.Lock()
387 self._per_shard_results = {}
388 self._storage = None
389
maruel12e30012015-10-09 11:55:35 -0700390 if self.task_output_dir and not fs.isdir(self.task_output_dir):
391 fs.makedirs(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700392
Vadim Shtayurab450c602014-05-12 19:23:25 -0700393 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700394 """Stores results of a single task shard, fetches output files if necessary.
395
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400396 Modifies |result| in place.
397
maruel77f720b2015-09-15 12:35:22 -0700398 shard_index is 0-based.
399
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700400 Called concurrently from multiple threads.
401 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700403 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700404 if shard_index < 0 or shard_index >= self.shard_count:
405 logging.warning(
406 'Shard index %d is outside of expected range: [0; %d]',
407 shard_index, self.shard_count - 1)
408 return
409
maruel77f720b2015-09-15 12:35:22 -0700410 if result.get('outputs_ref'):
411 ref = result['outputs_ref']
412 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
413 ref['isolatedserver'],
414 urllib.urlencode(
415 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400416
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700417 # Store result dict of that shard, ignore results we've already seen.
418 with self._lock:
419 if shard_index in self._per_shard_results:
420 logging.warning('Ignoring duplicate shard index %d', shard_index)
421 return
422 self._per_shard_results[shard_index] = result
423
424 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700425 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400426 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700427 result['outputs_ref']['isolatedserver'],
428 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400429 if storage:
430 # Output files are supposed to be small and they are not reused across
431 # tasks. So use MemoryCache for them instead of on-disk cache. Make
432 # files writable, so that calling script can delete them.
433 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700434 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400435 storage,
436 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -0700437 os.path.join(self.task_output_dir, str(shard_index)))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438
439 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441 with self._lock:
442 # Write an array of shard results with None for missing shards.
443 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444 'shards': [
445 self._per_shard_results.get(i) for i in xrange(self.shard_count)
446 ],
447 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700448 # Write summary.json to task_output_dir as well.
449 if self.task_output_dir:
450 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700451 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700452 summary,
453 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 if self._storage:
455 self._storage.close()
456 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700457 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700458
459 def _get_storage(self, isolate_server, namespace):
460 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700461 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700462 with self._lock:
463 if not self._storage:
464 self._storage = isolateserver.get_storage(isolate_server, namespace)
465 else:
466 # Shards must all use exact same isolate server and namespace.
467 if self._storage.location != isolate_server:
468 logging.error(
469 'Task shards are using multiple isolate servers: %s and %s',
470 self._storage.location, isolate_server)
471 return None
472 if self._storage.namespace != namespace:
473 logging.error(
474 'Task shards are using multiple namespaces: %s and %s',
475 self._storage.namespace, namespace)
476 return None
477 return self._storage
478
479
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500480def now():
481 """Exists so it can be mocked easily."""
482 return time.time()
483
484
maruel77f720b2015-09-15 12:35:22 -0700485def parse_time(value):
486 """Converts serialized time from the API to datetime.datetime."""
487 # When microseconds are 0, the '.123456' suffix is elided. This means the
488 # serialized format is not consistent, which confuses the hell out of python.
489 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
490 try:
491 return datetime.datetime.strptime(value, fmt)
492 except ValueError:
493 pass
494 raise ValueError('Failed to parse %s' % value)
495
496
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700497def retrieve_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400498 base_url, shard_index, task_id, timeout, should_stop, output_collector):
499 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700500
Vadim Shtayurab450c602014-05-12 19:23:25 -0700501 Returns:
502 <result dict> on success.
503 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700504 """
maruel71c61c82016-02-22 06:52:05 -0800505 assert timeout is None or isinstance(timeout, float), timeout
maruel77f720b2015-09-15 12:35:22 -0700506 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
507 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700508 started = now()
509 deadline = started + timeout if timeout else None
510 attempt = 0
511
512 while not should_stop.is_set():
513 attempt += 1
514
515 # Waiting for too long -> give up.
516 current_time = now()
517 if deadline and current_time >= deadline:
518 logging.error('retrieve_results(%s) timed out on attempt %d',
519 base_url, attempt)
520 return None
521
522 # Do not spin too fast. Spin faster at the beginning though.
523 # Start with 1 sec delay and for each 30 sec of waiting add another second
524 # of delay, until hitting 15 sec ceiling.
525 if attempt > 1:
526 max_delay = min(15, 1 + (current_time - started) / 30.0)
527 delay = min(max_delay, deadline - current_time) if deadline else max_delay
528 if delay > 0:
529 logging.debug('Waiting %.1f sec before retrying', delay)
530 should_stop.wait(delay)
531 if should_stop.is_set():
532 return None
533
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400534 # Disable internal retries in net.url_read_json, since we are doing retries
535 # ourselves.
536 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700537 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
538 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400539 result = net.url_read_json(result_url, retry_50x=False)
540 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400541 continue
maruel77f720b2015-09-15 12:35:22 -0700542
maruelbf53e042015-12-01 15:00:51 -0800543 if result.get('error'):
544 # An error occurred.
545 if result['error'].get('errors'):
546 for err in result['error']['errors']:
547 logging.warning(
548 'Error while reading task: %s; %s',
549 err.get('message'), err.get('debugInfo'))
550 elif result['error'].get('message'):
551 logging.warning(
552 'Error while reading task: %s', result['error']['message'])
553 continue
554
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400555 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700556 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400557 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700558 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700559 # Record the result, try to fetch attached output files (if any).
560 if output_collector:
561 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700562 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700563 if result.get('internal_failure'):
564 logging.error('Internal error!')
565 elif result['state'] == 'BOT_DIED':
566 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700567 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000568
569
maruel77f720b2015-09-15 12:35:22 -0700570def convert_to_old_format(result):
571 """Converts the task result data from Endpoints API format to old API format
572 for compatibility.
573
574 This goes into the file generated as --task-summary-json.
575 """
576 # Sets default.
577 result.setdefault('abandoned_ts', None)
578 result.setdefault('bot_id', None)
579 result.setdefault('bot_version', None)
580 result.setdefault('children_task_ids', [])
581 result.setdefault('completed_ts', None)
582 result.setdefault('cost_saved_usd', None)
583 result.setdefault('costs_usd', None)
584 result.setdefault('deduped_from', None)
585 result.setdefault('name', None)
586 result.setdefault('outputs_ref', None)
587 result.setdefault('properties_hash', None)
588 result.setdefault('server_versions', None)
589 result.setdefault('started_ts', None)
590 result.setdefault('tags', None)
591 result.setdefault('user', None)
592
593 # Convertion back to old API.
594 duration = result.pop('duration', None)
595 result['durations'] = [duration] if duration else []
596 exit_code = result.pop('exit_code', None)
597 result['exit_codes'] = [int(exit_code)] if exit_code else []
598 result['id'] = result.pop('task_id')
599 result['isolated_out'] = result.get('outputs_ref', None)
600 output = result.pop('output', None)
601 result['outputs'] = [output] if output else []
602 # properties_hash
603 # server_version
604 # Endpoints result 'state' as string. For compatibility with old code, convert
605 # to int.
606 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700607 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700608 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700609 if 'bot_dimensions' in result:
610 result['bot_dimensions'] = {
611 i['key']: i['value'] for i in result['bot_dimensions']
612 }
613 else:
614 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700615
616
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700617def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400618 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
619 output_collector):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500620 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000621
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700622 Duplicate shards are ignored. Shards are yielded in order of completion.
623 Timed out shards are NOT yielded at all. Caller can compare number of yielded
624 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625
626 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500627 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 +0000628 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500629
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700630 output_collector is an optional instance of TaskOutputCollector that will be
631 used to fetch files produced by a task from isolate server to the local disk.
632
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500633 Yields:
634 (index, result). In particular, 'result' is defined as the
635 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000636 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000637 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400638 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700639 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700640 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700641
maruel@chromium.org0437a732013-08-27 16:05:52 +0000642 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
643 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700644 # Adds a task to the thread pool to call 'retrieve_results' and return
645 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400646 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700647 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000648 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400649 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
650 task_id, timeout, should_stop, output_collector)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700651
652 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400653 for shard_index, task_id in enumerate(task_ids):
654 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700655
656 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400657 shards_remaining = range(len(task_ids))
658 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700659 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700660 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700661 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700662 shard_index, result = results_channel.pull(
663 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700664 except threading_utils.TaskChannel.Timeout:
665 if print_status_updates:
666 print(
667 'Waiting for results from the following shards: %s' %
668 ', '.join(map(str, shards_remaining)))
669 sys.stdout.flush()
670 continue
671 except Exception:
672 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700673
674 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700675 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000676 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500677 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000678 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700679
Vadim Shtayurab450c602014-05-12 19:23:25 -0700680 # Yield back results to the caller.
681 assert shard_index in shards_remaining
682 shards_remaining.remove(shard_index)
683 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700684
maruel@chromium.org0437a732013-08-27 16:05:52 +0000685 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700686 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000687 should_stop.set()
688
689
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400690def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000691 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700692 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400693 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700694 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
695 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400696 else:
697 pending = 'N/A'
698
maruel77f720b2015-09-15 12:35:22 -0700699 if metadata.get('duration') is not None:
700 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400701 else:
702 duration = 'N/A'
703
maruel77f720b2015-09-15 12:35:22 -0700704 if metadata.get('exit_code') is not None:
705 # Integers are encoded as string to not loose precision.
706 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400707 else:
708 exit_code = 'N/A'
709
710 bot_id = metadata.get('bot_id') or 'N/A'
711
maruel77f720b2015-09-15 12:35:22 -0700712 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400713 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400714 tag_footer = (
715 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
716 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400717
718 tag_len = max(len(tag_header), len(tag_footer))
719 dash_pad = '+-%s-+\n' % ('-' * tag_len)
720 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
721 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
722
723 header = dash_pad + tag_header + dash_pad
724 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700725 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400726 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000727
728
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700729def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700730 swarming, task_ids, timeout, decorate, print_status_updates,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400731 task_summary_json, task_output_dir):
maruela5490782015-09-30 10:56:59 -0700732 """Retrieves results of a Swarming task.
733
734 Returns:
735 process exit code that should be returned to the user.
736 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700737 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700738 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700739
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700740 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700741 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400742 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700743 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400744 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400745 swarming, task_ids, timeout, None, print_status_updates,
746 output_collector):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700747 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700748
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400749 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700750 shard_exit_code = metadata.get('exit_code')
751 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700752 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700753 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700754 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400755 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700756 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700757
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700758 if decorate:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400759 print(decorate_shard_output(swarming, index, metadata))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400760 if len(seen_shards) < len(task_ids):
761 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700762 else:
maruel77f720b2015-09-15 12:35:22 -0700763 print('%s: %s %s' % (
764 metadata.get('bot_id', 'N/A'),
765 metadata['task_id'],
766 shard_exit_code))
767 if metadata['output']:
768 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400769 if output:
770 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700771 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700772 summary = output_collector.finalize()
773 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700774 # TODO(maruel): Make this optional.
775 for i in summary['shards']:
776 if i:
777 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700778 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700779
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400780 if decorate and total_duration:
781 print('Total duration: %.1fs' % total_duration)
782
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400783 if len(seen_shards) != len(task_ids):
784 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700785 print >> sys.stderr, ('Results from some shards are missing: %s' %
786 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700787 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700788
maruela5490782015-09-30 10:56:59 -0700789 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000790
791
maruel77f720b2015-09-15 12:35:22 -0700792### API management.
793
794
795class APIError(Exception):
796 pass
797
798
799def endpoints_api_discovery_apis(host):
800 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
801 the APIs exposed by a host.
802
803 https://developers.google.com/discovery/v1/reference/apis/list
804 """
805 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
806 if data is None:
807 raise APIError('Failed to discover APIs on %s' % host)
808 out = {}
809 for api in data['items']:
810 if api['id'] == 'discovery:v1':
811 continue
812 # URL is of the following form:
813 # url = host + (
814 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
815 api_data = net.url_read_json(api['discoveryRestUrl'])
816 if api_data is None:
817 raise APIError('Failed to discover %s on %s' % (api['id'], host))
818 out[api['id']] = api_data
819 return out
820
821
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500822### Commands.
823
824
825def abort_task(_swarming, _manifest):
826 """Given a task manifest that was triggered, aborts its execution."""
827 # TODO(vadimsh): No supported by the server yet.
828
829
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400830def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400831 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500832 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500833 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500834 dest='dimensions', metavar='FOO bar',
835 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500836 parser.add_option_group(parser.filter_group)
837
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400838
Vadim Shtayurab450c602014-05-12 19:23:25 -0700839def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400840 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700841 parser.sharding_group.add_option(
842 '--shards', type='int', default=1,
843 help='Number of shards to trigger and collect.')
844 parser.add_option_group(parser.sharding_group)
845
846
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400847def add_trigger_options(parser):
848 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500849 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400850 add_filter_options(parser)
851
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400852 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500853 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500854 '-s', '--isolated',
855 help='Hash of the .isolated to grab from the isolate server')
856 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500857 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700858 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500859 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500860 '--priority', type='int', default=100,
861 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500862 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500863 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400864 help='Display name of the task. Defaults to '
865 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
866 'isolated file is provided, if a hash is provided, it defaults to '
867 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400868 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400869 '--tags', action='append', default=[],
870 help='Tags to assign to the task.')
871 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500872 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400873 help='User associated with the task. Defaults to authenticated user on '
874 'the server.')
875 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400876 '--idempotent', action='store_true', default=False,
877 help='When set, the server will actively try to find a previous task '
878 'with the same parameter and return this result instead if possible')
879 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400880 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400881 help='Seconds to allow the task to be pending for a bot to run before '
882 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400883 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400884 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400885 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400886 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400887 '--hard-timeout', type='int', default=60*60,
888 help='Seconds to allow the task to complete.')
889 parser.task_group.add_option(
890 '--io-timeout', type='int', default=20*60,
891 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500892 parser.task_group.add_option(
893 '--raw-cmd', action='store_true', default=False,
894 help='When set, the command after -- is used as-is without run_isolated. '
895 'In this case, no .isolated file is expected.')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500896 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000897
898
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500899def process_trigger_options(parser, options, args):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500900 """Processes trigger options and uploads files to isolate server if necessary.
901 """
902 options.dimensions = dict(options.dimensions)
903 options.env = dict(options.env)
904
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500905 if not options.dimensions:
906 parser.error('Please at least specify one --dimension')
907 if options.raw_cmd:
908 if not args:
909 parser.error(
910 'Arguments with --raw-cmd should be passed after -- as command '
911 'delimiter.')
912 if options.isolate_server:
913 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
914
915 command = args
916 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500917 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500918 options.user,
919 '_'.join(
920 '%s=%s' % (k, v)
921 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700922 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500923 else:
924 isolateserver.process_isolate_server_options(parser, options, False)
925 try:
maruel77f720b2015-09-15 12:35:22 -0700926 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500927 except ValueError as e:
928 parser.error(str(e))
929
maruel77f720b2015-09-15 12:35:22 -0700930 # If inputs_ref is used, command is actually extra_args. Otherwise it's an
931 # actual command to run.
932 properties = TaskProperties(
933 command=None if inputs_ref else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500934 dimensions=options.dimensions,
935 env=options.env,
maruel77f720b2015-09-15 12:35:22 -0700936 execution_timeout_secs=options.hard_timeout,
937 extra_args=command if inputs_ref else None,
938 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500939 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -0700940 inputs_ref=inputs_ref,
941 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -0700942 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
943 parser.error('--tags must be in the format key:value')
maruel77f720b2015-09-15 12:35:22 -0700944 return NewTaskRequest(
945 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500946 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -0700947 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500948 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -0700949 properties=properties,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500950 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -0700951 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000952
953
954def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500955 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -0800956 '-t', '--timeout', type='float',
957 help='Timeout to wait for result, set to 0 for no timeout; default to no '
958 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500959 parser.group_logging.add_option(
960 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700961 parser.group_logging.add_option(
962 '--print-status-updates', action='store_true',
963 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400964 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700965 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700966 '--task-summary-json',
967 metavar='FILE',
968 help='Dump a summary of task results to this file as json. It contains '
969 'only shards statuses as know to server directly. Any output files '
970 'emitted by the task can be collected by using --task-output-dir')
971 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700972 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700973 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700974 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700975 'directory contains per-shard directory with output files produced '
976 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700977 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000978
979
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -0400980@subcommand.usage('bots...')
981def CMDbot_delete(parser, args):
982 """Forcibly deletes bots from the Swarming server."""
983 parser.add_option(
984 '-f', '--force', action='store_true',
985 help='Do not prompt for confirmation')
986 options, args = parser.parse_args(args)
987 if not args:
988 parser.error('Please specific bots to delete')
989
990 bots = sorted(args)
991 if not options.force:
992 print('Delete the following bots?')
993 for bot in bots:
994 print(' %s' % bot)
995 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
996 print('Goodbye.')
997 return 1
998
999 result = 0
1000 for bot in bots:
vadimshe4c0e242015-09-30 11:53:54 -07001001 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
1002 if net.url_read_json(url, data={}, method='POST') is None:
1003 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001004 result = 1
1005 return result
1006
1007
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001008def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001009 """Returns information about the bots connected to the Swarming server."""
1010 add_filter_options(parser)
1011 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001012 '--dead-only', action='store_true',
1013 help='Only print dead bots, useful to reap them and reimage broken bots')
1014 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001015 '-k', '--keep-dead', action='store_true',
1016 help='Do not filter out dead bots')
1017 parser.filter_group.add_option(
1018 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001019 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001020 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001021
1022 if options.keep_dead and options.dead_only:
1023 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001024
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001025 bots = []
1026 cursor = None
1027 limit = 250
1028 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001029 base_url = (
1030 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001031 while True:
1032 url = base_url
1033 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001034 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001035 data = net.url_read_json(url)
1036 if data is None:
1037 print >> sys.stderr, 'Failed to access %s' % options.swarming
1038 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001039 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001040 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001041 if not cursor:
1042 break
1043
maruel77f720b2015-09-15 12:35:22 -07001044 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001045 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001046 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001047 continue
maruel77f720b2015-09-15 12:35:22 -07001048 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001049 continue
1050
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001051 # If the user requested to filter on dimensions, ensure the bot has all the
1052 # dimensions requested.
maruel6c9a3372016-01-12 10:27:04 -08001053 dimensions = {i['key']: i.get('value') for i in bot['dimensions']}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001054 for key, value in options.dimensions:
1055 if key not in dimensions:
1056 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001057 # A bot can have multiple value for a key, for example,
1058 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1059 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001060 if isinstance(dimensions[key], list):
1061 if value not in dimensions[key]:
1062 break
1063 else:
1064 if value != dimensions[key]:
1065 break
1066 else:
maruel77f720b2015-09-15 12:35:22 -07001067 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001068 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001069 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001070 if bot.get('task_id'):
1071 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001072 return 0
1073
1074
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001075@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001076def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001077 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001078
1079 The result can be in multiple part if the execution was sharded. It can
1080 potentially have retries.
1081 """
1082 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001083 parser.add_option(
1084 '-j', '--json',
1085 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001086 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001087 if not args and not options.json:
1088 parser.error('Must specify at least one task id or --json.')
1089 if args and options.json:
1090 parser.error('Only use one of task id or --json.')
1091
1092 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001093 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001094 try:
maruel1ceb3872015-10-14 06:10:44 -07001095 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001096 data = json.load(f)
1097 except (IOError, ValueError):
1098 parser.error('Failed to open %s' % options.json)
1099 try:
1100 tasks = sorted(
1101 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1102 args = [t['task_id'] for t in tasks]
1103 except (KeyError, TypeError):
1104 parser.error('Failed to process %s' % options.json)
1105 if options.timeout is None:
1106 options.timeout = (
1107 data['request']['properties']['execution_timeout_secs'] +
1108 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001109 else:
1110 valid = frozenset('0123456789abcdef')
1111 if any(not valid.issuperset(task_id) for task_id in args):
1112 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001113
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001114 try:
1115 return collect(
1116 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001117 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001118 options.timeout,
1119 options.decorate,
1120 options.print_status_updates,
1121 options.task_summary_json,
1122 options.task_output_dir)
1123 except Failure:
1124 on_error.report(None)
1125 return 1
1126
1127
maruelbea00862015-09-18 09:55:36 -07001128@subcommand.usage('[filename]')
1129def CMDput_bootstrap(parser, args):
1130 """Uploads a new version of bootstrap.py."""
1131 options, args = parser.parse_args(args)
1132 if len(args) != 1:
1133 parser.error('Must specify file to upload')
1134 url = options.swarming + '/_ah/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001135 path = unicode(os.path.abspath(args[0]))
1136 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001137 content = f.read().decode('utf-8')
1138 data = net.url_read_json(url, data={'content': content})
1139 print data
1140 return 0
1141
1142
1143@subcommand.usage('[filename]')
1144def CMDput_bot_config(parser, args):
1145 """Uploads a new version of bot_config.py."""
1146 options, args = parser.parse_args(args)
1147 if len(args) != 1:
1148 parser.error('Must specify file to upload')
1149 url = options.swarming + '/_ah/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001150 path = unicode(os.path.abspath(args[0]))
1151 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001152 content = f.read().decode('utf-8')
1153 data = net.url_read_json(url, data={'content': content})
1154 print data
1155 return 0
1156
1157
maruel77f720b2015-09-15 12:35:22 -07001158@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001159def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001160 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1161 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001162
1163 Examples:
maruel77f720b2015-09-15 12:35:22 -07001164 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001165 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001166
maruel77f720b2015-09-15 12:35:22 -07001167 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001168 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1169
1170 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1171 quoting is important!:
1172 swarming.py query -S server-url.com --limit 10 \\
1173 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001174 """
1175 CHUNK_SIZE = 250
1176
1177 parser.add_option(
1178 '-L', '--limit', type='int', default=200,
1179 help='Limit to enforce on limitless items (like number of tasks); '
1180 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001181 parser.add_option(
1182 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001183 parser.add_option(
1184 '--progress', action='store_true',
1185 help='Prints a dot at each request to show progress')
1186 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001187 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001188 parser.error(
1189 'Must specify only method name and optionally query args properly '
1190 'escaped.')
1191 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001192 url = base_url
1193 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001194 # Check check, change if not working out.
1195 merge_char = '&' if '?' in url else '?'
1196 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001197 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001198 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001199 # TODO(maruel): Do basic diagnostic.
1200 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001201 return 1
1202
1203 # Some items support cursors. Try to get automatically if cursors are needed
1204 # by looking at the 'cursor' items.
1205 while (
1206 data.get('cursor') and
1207 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001208 merge_char = '&' if '?' in base_url else '?'
1209 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001210 if options.limit:
1211 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001212 if options.progress:
1213 sys.stdout.write('.')
1214 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001215 new = net.url_read_json(url)
1216 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001217 if options.progress:
1218 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001219 print >> sys.stderr, 'Failed to access %s' % options.swarming
1220 return 1
maruel81b37132015-10-21 06:42:13 -07001221 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001222 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001223
maruel77f720b2015-09-15 12:35:22 -07001224 if options.progress:
1225 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001226 if options.limit and len(data.get('items', [])) > options.limit:
1227 data['items'] = data['items'][:options.limit]
1228 data.pop('cursor', None)
1229
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001230 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001231 options.json = unicode(os.path.abspath(options.json))
1232 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001233 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001234 try:
maruel77f720b2015-09-15 12:35:22 -07001235 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001236 sys.stdout.write('\n')
1237 except IOError:
1238 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001239 return 0
1240
1241
maruel77f720b2015-09-15 12:35:22 -07001242def CMDquery_list(parser, args):
1243 """Returns list of all the Swarming APIs that can be used with command
1244 'query'.
1245 """
1246 parser.add_option(
1247 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1248 options, args = parser.parse_args(args)
1249 if args:
1250 parser.error('No argument allowed.')
1251
1252 try:
1253 apis = endpoints_api_discovery_apis(options.swarming)
1254 except APIError as e:
1255 parser.error(str(e))
1256 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001257 options.json = unicode(os.path.abspath(options.json))
1258 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001259 json.dump(apis, f)
1260 else:
1261 help_url = (
1262 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1263 options.swarming)
1264 for api_id, api in sorted(apis.iteritems()):
1265 print api_id
1266 print ' ' + api['description']
1267 for resource_name, resource in sorted(api['resources'].iteritems()):
1268 print ''
1269 for method_name, method in sorted(resource['methods'].iteritems()):
1270 # Only list the GET ones.
1271 if method['httpMethod'] != 'GET':
1272 continue
1273 print '- %s.%s: %s' % (
1274 resource_name, method_name, method['path'])
1275 print ' ' + method['description']
1276 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1277 return 0
1278
1279
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001280@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001281def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001282 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001283
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001284 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001285 """
1286 add_trigger_options(parser)
1287 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001288 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001289 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001290 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001291 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001292 tasks = trigger_task_shards(
1293 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001294 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001295 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001296 'Failed to trigger %s(%s): %s' %
1297 (options.task_name, args[0], e.args[0]))
1298 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001299 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001300 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001301 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001302 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001303 task_ids = [
1304 t['task_id']
1305 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1306 ]
maruel71c61c82016-02-22 06:52:05 -08001307 if options.timeout is None:
1308 options.timeout = (
1309 task_request.properties.execution_timeout_secs +
1310 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001311 try:
1312 return collect(
1313 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001314 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001315 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001316 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001317 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001318 options.task_summary_json,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001319 options.task_output_dir)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001320 except Failure:
1321 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001322 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001323
1324
maruel18122c62015-10-23 06:31:23 -07001325@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001326def CMDreproduce(parser, args):
1327 """Runs a task locally that was triggered on the server.
1328
1329 This running locally the same commands that have been run on the bot. The data
1330 downloaded will be in a subdirectory named 'work' of the current working
1331 directory.
maruel18122c62015-10-23 06:31:23 -07001332
1333 You can pass further additional arguments to the target command by passing
1334 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001335 """
maruelc070e672016-02-22 17:32:57 -08001336 parser.add_option(
1337 '--output-dir', metavar='DIR', default='',
1338 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001339 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001340 extra_args = []
1341 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001342 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001343 if len(args) > 1:
1344 if args[1] == '--':
1345 if len(args) > 2:
1346 extra_args = args[2:]
1347 else:
1348 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001349
maruel77f720b2015-09-15 12:35:22 -07001350 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001351 request = net.url_read_json(url)
1352 if not request:
1353 print >> sys.stderr, 'Failed to retrieve request data for the task'
1354 return 1
1355
maruel12e30012015-10-09 11:55:35 -07001356 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001357 if fs.isdir(workdir):
1358 parser.error('Please delete the directory \'work\' first')
1359 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001360
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001361 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001362 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001363 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001364 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001365 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001366 for i in properties['env']:
1367 key = i['key'].encode('utf-8')
1368 if not i['value']:
1369 env.pop(key, None)
1370 else:
1371 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001372
maruel29ab2fd2015-10-16 11:44:01 -07001373 if properties.get('inputs_ref'):
1374 # Create the tree.
1375 with isolateserver.get_storage(
1376 properties['inputs_ref']['isolatedserver'],
1377 properties['inputs_ref']['namespace']) as storage:
1378 bundle = isolateserver.fetch_isolated(
1379 properties['inputs_ref']['isolated'],
1380 storage,
1381 isolateserver.MemoryCache(file_mode_mask=0700),
maruelb8d88d12016-04-08 12:54:01 -07001382 workdir)
maruel29ab2fd2015-10-16 11:44:01 -07001383 command = bundle.command
1384 if bundle.relative_cwd:
1385 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001386 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001387 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1388 new_command = run_isolated.process_command(command, options.output_dir)
1389 if not options.output_dir and new_command != command:
1390 parser.error('The task has outputs, you must use --output-dir')
1391 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001392 else:
1393 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001394 try:
maruel18122c62015-10-23 06:31:23 -07001395 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001396 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001397 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001398 print >> sys.stderr, str(e)
1399 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001400
1401
maruel0eb1d1b2015-10-02 14:48:21 -07001402@subcommand.usage('bot_id')
1403def CMDterminate(parser, args):
1404 """Tells a bot to gracefully shut itself down as soon as it can.
1405
1406 This is done by completing whatever current task there is then exiting the bot
1407 process.
1408 """
1409 parser.add_option(
1410 '--wait', action='store_true', help='Wait for the bot to terminate')
1411 options, args = parser.parse_args(args)
1412 if len(args) != 1:
1413 parser.error('Please provide the bot id')
1414 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
1415 request = net.url_read_json(url, data={})
1416 if not request:
1417 print >> sys.stderr, 'Failed to ask for termination'
1418 return 1
1419 if options.wait:
1420 return collect(
1421 options.swarming, [request['task_id']], 0., False, False, None, None)
1422 return 0
1423
1424
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001425@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001426def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001427 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001428
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001429 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001430 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001431
1432 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001433
1434 Passes all extra arguments provided after '--' as additional command line
1435 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001436 """
1437 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001438 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001439 parser.add_option(
1440 '--dump-json',
1441 metavar='FILE',
1442 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001443 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001444 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001445 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001446 tasks = trigger_task_shards(
1447 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001448 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001449 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001450 tasks_sorted = sorted(
1451 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001452 if options.dump_json:
1453 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001454 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001455 'tasks': tasks,
maruel71c61c82016-02-22 06:52:05 -08001456 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001457 }
maruel46b015f2015-10-13 18:40:35 -07001458 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001459 print('To collect results, use:')
1460 print(' swarming.py collect -S %s --json %s' %
1461 (options.swarming, options.dump_json))
1462 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001463 print('To collect results, use:')
1464 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001465 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1466 print('Or visit:')
1467 for t in tasks_sorted:
1468 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001469 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001470 except Failure:
1471 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001472 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001473
1474
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001475class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001476 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001477 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001478 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001479 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001480 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001481 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001482 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001483 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001484 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001485 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001486
1487 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001488 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001489 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001490 auth.process_auth_options(self, options)
1491 user = self._process_swarming(options)
1492 if hasattr(options, 'user') and not options.user:
1493 options.user = user
1494 return options, args
1495
1496 def _process_swarming(self, options):
1497 """Processes the --swarming option and aborts if not specified.
1498
1499 Returns the identity as determined by the server.
1500 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001501 if not options.swarming:
1502 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001503 try:
1504 options.swarming = net.fix_url(options.swarming)
1505 except ValueError as e:
1506 self.error('--swarming %s' % e)
1507 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001508 try:
1509 user = auth.ensure_logged_in(options.swarming)
1510 except ValueError as e:
1511 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001512 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001513
1514
1515def main(args):
1516 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001517 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001518
1519
1520if __name__ == '__main__':
1521 fix_encoding.fix_encoding()
1522 tools.disable_buffering()
1523 colorama.init()
1524 sys.exit(main(sys.argv[1:]))