blob: 1f95b635ddcfe550a4b2be4126ed627c00398a32 [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
vadimsh93d167c2016-09-13 11:31:51 -07008__version__ = '0.8.7'
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
Vadim Shtayurab19319e2014-04-27 08:50:06 -070018import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000019import time
20import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000021
22from third_party import colorama
23from third_party.depot_tools import fix_encoding
24from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000025
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050026from utils import file_path
maruel12e30012015-10-09 11:55:35 -070027from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040028from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040029from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000030from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040031from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070032from utils import subprocess42
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
tansella4949442016-06-23 22:34:32 -070042ROOT_DIR = os.path.dirname(os.path.abspath(
43 __file__.decode(sys.getfilesystemencoding())))
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',
vadimsh93d167c2016-09-13 11:31:51 -0700190 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500191 'tags',
192 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500193 ])
194
195
maruel77f720b2015-09-15 12:35:22 -0700196def namedtuple_to_dict(value):
197 """Recursively converts a namedtuple to a dict."""
198 out = dict(value._asdict())
199 for k, v in out.iteritems():
200 if hasattr(v, '_asdict'):
201 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700202 elif isinstance(v, (list, tuple)):
203 l = []
204 for elem in v:
205 if hasattr(elem, '_asdict'):
206 l.append(namedtuple_to_dict(elem))
207 else:
208 l.append(elem)
209 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700210 return out
211
212
vadimsh93d167c2016-09-13 11:31:51 -0700213def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800214 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700215
216 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500217 """
maruel77f720b2015-09-15 12:35:22 -0700218 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700219 if hide_token:
220 if out['service_account_token'] not in (None, 'bot', 'none'):
221 out['service_account_token'] = '<hidden>'
222 # Don't send 'service_account_token' if it is None to avoid confusing older
223 # version of the server that doesn't know about 'service_account_token'.
224 if out['service_account_token'] in (None, 'none'):
225 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700226 # Maps are not supported until protobuf v3.
227 out['properties']['dimensions'] = [
228 {'key': k, 'value': v}
229 for k, v in out['properties']['dimensions'].iteritems()
230 ]
231 out['properties']['dimensions'].sort(key=lambda x: x['key'])
232 out['properties']['env'] = [
233 {'key': k, 'value': v}
234 for k, v in out['properties']['env'].iteritems()
235 ]
236 out['properties']['env'].sort(key=lambda x: x['key'])
237 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500238
239
maruel77f720b2015-09-15 12:35:22 -0700240def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500241 """Triggers a request on the Swarming server and returns the json data.
242
243 It's the low-level function.
244
245 Returns:
246 {
247 'request': {
248 'created_ts': u'2010-01-02 03:04:05',
249 'name': ..
250 },
251 'task_id': '12300',
252 }
253 """
254 logging.info('Triggering: %s', raw_request['name'])
255
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500256 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700257 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500258 if not result:
259 on_error.report('Failed to trigger task %s' % raw_request['name'])
260 return None
maruele557bce2015-11-17 09:01:27 -0800261 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800262 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800263 msg = 'Failed to trigger task %s' % raw_request['name']
264 if result['error'].get('errors'):
265 for err in result['error']['errors']:
266 if err.get('message'):
267 msg += '\nMessage: %s' % err['message']
268 if err.get('debugInfo'):
269 msg += '\nDebug info:\n%s' % err['debugInfo']
270 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800271 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800272
273 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800274 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500275 return result
276
277
278def setup_googletest(env, shards, index):
279 """Sets googletest specific environment variables."""
280 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700281 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
282 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
283 env = env[:]
284 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
285 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500286 return env
287
288
289def trigger_task_shards(swarming, task_request, shards):
290 """Triggers one or many subtasks of a sharded task.
291
292 Returns:
293 Dict with task details, returned to caller as part of --dump-json output.
294 None in case of failure.
295 """
296 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700297 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500298 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700299 req['properties']['env'] = setup_googletest(
300 req['properties']['env'], shards, index)
301 req['name'] += ':%s:%s' % (index, shards)
302 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500303
304 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500305 tasks = {}
306 priority_warning = False
307 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700308 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500309 if not task:
310 break
311 logging.info('Request result: %s', task)
312 if (not priority_warning and
313 task['request']['priority'] != task_request.priority):
314 priority_warning = True
315 print >> sys.stderr, (
316 'Priority was reset to %s' % task['request']['priority'])
317 tasks[request['name']] = {
318 'shard_index': index,
319 'task_id': task['task_id'],
320 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
321 }
322
323 # Some shards weren't triggered. Abort everything.
324 if len(tasks) != len(requests):
325 if tasks:
326 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
327 len(tasks), len(requests))
328 for task_dict in tasks.itervalues():
329 abort_task(swarming, task_dict['task_id'])
330 return None
331
332 return tasks
333
334
vadimsh93d167c2016-09-13 11:31:51 -0700335def mint_service_account_token(service_account):
336 """Given a service account name returns a delegation token for this account.
337
338 The token is generated based on triggering user's credentials. It is passed
339 to Swarming, that uses it when running tasks.
340 """
341 logging.info(
342 'Generating delegation token for service account "%s"', service_account)
343 raise NotImplementedError('Custom service accounts are not implemented yet')
344
345
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500346### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000347
348
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700349# How often to print status updates to stdout in 'collect'.
350STATUS_UPDATE_INTERVAL = 15 * 60.
351
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400352
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400353class State(object):
354 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000355
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400356 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
357 values are part of the API so if they change, the API changed.
358
359 It's in fact an enum. Values should be in decreasing order of importance.
360 """
361 RUNNING = 0x10
362 PENDING = 0x20
363 EXPIRED = 0x30
364 TIMED_OUT = 0x40
365 BOT_DIED = 0x50
366 CANCELED = 0x60
367 COMPLETED = 0x70
368
maruel77f720b2015-09-15 12:35:22 -0700369 STATES = (
370 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
371 'COMPLETED')
372 STATES_RUNNING = ('RUNNING', 'PENDING')
373 STATES_NOT_RUNNING = (
374 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
375 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
376 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400377
378 _NAMES = {
379 RUNNING: 'Running',
380 PENDING: 'Pending',
381 EXPIRED: 'Expired',
382 TIMED_OUT: 'Execution timed out',
383 BOT_DIED: 'Bot died',
384 CANCELED: 'User canceled',
385 COMPLETED: 'Completed',
386 }
387
maruel77f720b2015-09-15 12:35:22 -0700388 _ENUMS = {
389 'RUNNING': RUNNING,
390 'PENDING': PENDING,
391 'EXPIRED': EXPIRED,
392 'TIMED_OUT': TIMED_OUT,
393 'BOT_DIED': BOT_DIED,
394 'CANCELED': CANCELED,
395 'COMPLETED': COMPLETED,
396 }
397
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400398 @classmethod
399 def to_string(cls, state):
400 """Returns a user-readable string representing a State."""
401 if state not in cls._NAMES:
402 raise ValueError('Invalid state %s' % state)
403 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000404
maruel77f720b2015-09-15 12:35:22 -0700405 @classmethod
406 def from_enum(cls, state):
407 """Returns int value based on the string."""
408 if state not in cls._ENUMS:
409 raise ValueError('Invalid state %s' % state)
410 return cls._ENUMS[state]
411
maruel@chromium.org0437a732013-08-27 16:05:52 +0000412
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700413class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700414 """Assembles task execution summary (for --task-summary-json output).
415
416 Optionally fetches task outputs from isolate server to local disk (used when
417 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700418
419 This object is shared among multiple threads running 'retrieve_results'
420 function, in particular they call 'process_shard_result' method in parallel.
421 """
422
maruel0eb1d1b2015-10-02 14:48:21 -0700423 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
425
426 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700427 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700428 shard_count: expected number of task shards.
429 """
maruel12e30012015-10-09 11:55:35 -0700430 self.task_output_dir = (
431 unicode(os.path.abspath(task_output_dir))
432 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 self.shard_count = shard_count
434
435 self._lock = threading.Lock()
436 self._per_shard_results = {}
437 self._storage = None
438
nodire5028a92016-04-29 14:38:21 -0700439 if self.task_output_dir:
440 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441
Vadim Shtayurab450c602014-05-12 19:23:25 -0700442 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 """Stores results of a single task shard, fetches output files if necessary.
444
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400445 Modifies |result| in place.
446
maruel77f720b2015-09-15 12:35:22 -0700447 shard_index is 0-based.
448
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700449 Called concurrently from multiple threads.
450 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700451 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700452 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700453 if shard_index < 0 or shard_index >= self.shard_count:
454 logging.warning(
455 'Shard index %d is outside of expected range: [0; %d]',
456 shard_index, self.shard_count - 1)
457 return
458
maruel77f720b2015-09-15 12:35:22 -0700459 if result.get('outputs_ref'):
460 ref = result['outputs_ref']
461 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
462 ref['isolatedserver'],
463 urllib.urlencode(
464 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400465
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700466 # Store result dict of that shard, ignore results we've already seen.
467 with self._lock:
468 if shard_index in self._per_shard_results:
469 logging.warning('Ignoring duplicate shard index %d', shard_index)
470 return
471 self._per_shard_results[shard_index] = result
472
473 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700474 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400475 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700476 result['outputs_ref']['isolatedserver'],
477 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400478 if storage:
479 # Output files are supposed to be small and they are not reused across
480 # tasks. So use MemoryCache for them instead of on-disk cache. Make
481 # files writable, so that calling script can delete them.
482 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700483 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400484 storage,
485 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700486 os.path.join(self.task_output_dir, str(shard_index)),
487 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700488
489 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700490 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700491 with self._lock:
492 # Write an array of shard results with None for missing shards.
493 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700494 'shards': [
495 self._per_shard_results.get(i) for i in xrange(self.shard_count)
496 ],
497 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700498 # Write summary.json to task_output_dir as well.
499 if self.task_output_dir:
500 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700501 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700502 summary,
503 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700504 if self._storage:
505 self._storage.close()
506 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700507 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700508
509 def _get_storage(self, isolate_server, namespace):
510 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700511 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700512 with self._lock:
513 if not self._storage:
514 self._storage = isolateserver.get_storage(isolate_server, namespace)
515 else:
516 # Shards must all use exact same isolate server and namespace.
517 if self._storage.location != isolate_server:
518 logging.error(
519 'Task shards are using multiple isolate servers: %s and %s',
520 self._storage.location, isolate_server)
521 return None
522 if self._storage.namespace != namespace:
523 logging.error(
524 'Task shards are using multiple namespaces: %s and %s',
525 self._storage.namespace, namespace)
526 return None
527 return self._storage
528
529
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500530def now():
531 """Exists so it can be mocked easily."""
532 return time.time()
533
534
maruel77f720b2015-09-15 12:35:22 -0700535def parse_time(value):
536 """Converts serialized time from the API to datetime.datetime."""
537 # When microseconds are 0, the '.123456' suffix is elided. This means the
538 # serialized format is not consistent, which confuses the hell out of python.
539 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
540 try:
541 return datetime.datetime.strptime(value, fmt)
542 except ValueError:
543 pass
544 raise ValueError('Failed to parse %s' % value)
545
546
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700547def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700548 base_url, shard_index, task_id, timeout, should_stop, output_collector,
549 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400550 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700551
Vadim Shtayurab450c602014-05-12 19:23:25 -0700552 Returns:
553 <result dict> on success.
554 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700555 """
maruel71c61c82016-02-22 06:52:05 -0800556 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700557 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700558 if include_perf:
559 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700560 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700561 started = now()
562 deadline = started + timeout if timeout else None
563 attempt = 0
564
565 while not should_stop.is_set():
566 attempt += 1
567
568 # Waiting for too long -> give up.
569 current_time = now()
570 if deadline and current_time >= deadline:
571 logging.error('retrieve_results(%s) timed out on attempt %d',
572 base_url, attempt)
573 return None
574
575 # Do not spin too fast. Spin faster at the beginning though.
576 # Start with 1 sec delay and for each 30 sec of waiting add another second
577 # of delay, until hitting 15 sec ceiling.
578 if attempt > 1:
579 max_delay = min(15, 1 + (current_time - started) / 30.0)
580 delay = min(max_delay, deadline - current_time) if deadline else max_delay
581 if delay > 0:
582 logging.debug('Waiting %.1f sec before retrying', delay)
583 should_stop.wait(delay)
584 if should_stop.is_set():
585 return None
586
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400587 # Disable internal retries in net.url_read_json, since we are doing retries
588 # ourselves.
589 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700590 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
591 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400592 result = net.url_read_json(result_url, retry_50x=False)
593 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400594 continue
maruel77f720b2015-09-15 12:35:22 -0700595
maruelbf53e042015-12-01 15:00:51 -0800596 if result.get('error'):
597 # An error occurred.
598 if result['error'].get('errors'):
599 for err in result['error']['errors']:
600 logging.warning(
601 'Error while reading task: %s; %s',
602 err.get('message'), err.get('debugInfo'))
603 elif result['error'].get('message'):
604 logging.warning(
605 'Error while reading task: %s', result['error']['message'])
606 continue
607
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400608 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700609 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400610 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700611 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700612 # Record the result, try to fetch attached output files (if any).
613 if output_collector:
614 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700615 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700616 if result.get('internal_failure'):
617 logging.error('Internal error!')
618 elif result['state'] == 'BOT_DIED':
619 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700620 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000621
622
maruel77f720b2015-09-15 12:35:22 -0700623def convert_to_old_format(result):
624 """Converts the task result data from Endpoints API format to old API format
625 for compatibility.
626
627 This goes into the file generated as --task-summary-json.
628 """
629 # Sets default.
630 result.setdefault('abandoned_ts', None)
631 result.setdefault('bot_id', None)
632 result.setdefault('bot_version', None)
633 result.setdefault('children_task_ids', [])
634 result.setdefault('completed_ts', None)
635 result.setdefault('cost_saved_usd', None)
636 result.setdefault('costs_usd', None)
637 result.setdefault('deduped_from', None)
638 result.setdefault('name', None)
639 result.setdefault('outputs_ref', None)
640 result.setdefault('properties_hash', None)
641 result.setdefault('server_versions', None)
642 result.setdefault('started_ts', None)
643 result.setdefault('tags', None)
644 result.setdefault('user', None)
645
646 # Convertion back to old API.
647 duration = result.pop('duration', None)
648 result['durations'] = [duration] if duration else []
649 exit_code = result.pop('exit_code', None)
650 result['exit_codes'] = [int(exit_code)] if exit_code else []
651 result['id'] = result.pop('task_id')
652 result['isolated_out'] = result.get('outputs_ref', None)
653 output = result.pop('output', None)
654 result['outputs'] = [output] if output else []
655 # properties_hash
656 # server_version
657 # Endpoints result 'state' as string. For compatibility with old code, convert
658 # to int.
659 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700660 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700661 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700662 if 'bot_dimensions' in result:
663 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700664 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700665 }
666 else:
667 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700668
669
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700670def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400671 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700672 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500673 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000674
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700675 Duplicate shards are ignored. Shards are yielded in order of completion.
676 Timed out shards are NOT yielded at all. Caller can compare number of yielded
677 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000678
679 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500680 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 +0000681 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500682
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700683 output_collector is an optional instance of TaskOutputCollector that will be
684 used to fetch files produced by a task from isolate server to the local disk.
685
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500686 Yields:
687 (index, result). In particular, 'result' is defined as the
688 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000689 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400691 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700692 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700693 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700694
maruel@chromium.org0437a732013-08-27 16:05:52 +0000695 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
696 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700697 # Adds a task to the thread pool to call 'retrieve_results' and return
698 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400699 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700700 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000701 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400702 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700703 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700704
705 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400706 for shard_index, task_id in enumerate(task_ids):
707 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700708
709 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400710 shards_remaining = range(len(task_ids))
711 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700712 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700713 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700714 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700715 shard_index, result = results_channel.pull(
716 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700717 except threading_utils.TaskChannel.Timeout:
718 if print_status_updates:
719 print(
720 'Waiting for results from the following shards: %s' %
721 ', '.join(map(str, shards_remaining)))
722 sys.stdout.flush()
723 continue
724 except Exception:
725 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700726
727 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700728 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000729 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500730 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000731 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700732
Vadim Shtayurab450c602014-05-12 19:23:25 -0700733 # Yield back results to the caller.
734 assert shard_index in shards_remaining
735 shards_remaining.remove(shard_index)
736 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700737
maruel@chromium.org0437a732013-08-27 16:05:52 +0000738 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700739 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000740 should_stop.set()
741
742
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400743def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000744 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700745 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400746 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700747 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
748 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400749 else:
750 pending = 'N/A'
751
maruel77f720b2015-09-15 12:35:22 -0700752 if metadata.get('duration') is not None:
753 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400754 else:
755 duration = 'N/A'
756
maruel77f720b2015-09-15 12:35:22 -0700757 if metadata.get('exit_code') is not None:
758 # Integers are encoded as string to not loose precision.
759 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400760 else:
761 exit_code = 'N/A'
762
763 bot_id = metadata.get('bot_id') or 'N/A'
764
maruel77f720b2015-09-15 12:35:22 -0700765 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400766 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400767 tag_footer = (
768 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
769 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400770
771 tag_len = max(len(tag_header), len(tag_footer))
772 dash_pad = '+-%s-+\n' % ('-' * tag_len)
773 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
774 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
775
776 header = dash_pad + tag_header + dash_pad
777 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700778 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400779 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000780
781
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700783 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700784 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700785 """Retrieves results of a Swarming task.
786
787 Returns:
788 process exit code that should be returned to the user.
789 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700790 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700791 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700792
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700793 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700794 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400795 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700796 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400797 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400798 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700799 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700800 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700801
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400802 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700803 shard_exit_code = metadata.get('exit_code')
804 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700805 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700806 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700807 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400808 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700809 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700810
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700811 if decorate:
leileied181762016-10-13 14:24:59 -0700812 s = decorate_shard_output(swarming, index, metadata).encode(
813 'utf-8', 'replace')
814 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400815 if len(seen_shards) < len(task_ids):
816 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700817 else:
maruel77f720b2015-09-15 12:35:22 -0700818 print('%s: %s %s' % (
819 metadata.get('bot_id', 'N/A'),
820 metadata['task_id'],
821 shard_exit_code))
822 if metadata['output']:
823 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400824 if output:
825 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700826 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700827 summary = output_collector.finalize()
828 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700829 # TODO(maruel): Make this optional.
830 for i in summary['shards']:
831 if i:
832 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700833 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700834
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400835 if decorate and total_duration:
836 print('Total duration: %.1fs' % total_duration)
837
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400838 if len(seen_shards) != len(task_ids):
839 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700840 print >> sys.stderr, ('Results from some shards are missing: %s' %
841 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700842 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700843
maruela5490782015-09-30 10:56:59 -0700844 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000845
846
maruel77f720b2015-09-15 12:35:22 -0700847### API management.
848
849
850class APIError(Exception):
851 pass
852
853
854def endpoints_api_discovery_apis(host):
855 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
856 the APIs exposed by a host.
857
858 https://developers.google.com/discovery/v1/reference/apis/list
859 """
maruel380e3262016-08-31 16:10:06 -0700860 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
861 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700862 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
863 if data is None:
864 raise APIError('Failed to discover APIs on %s' % host)
865 out = {}
866 for api in data['items']:
867 if api['id'] == 'discovery:v1':
868 continue
869 # URL is of the following form:
870 # url = host + (
871 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
872 api_data = net.url_read_json(api['discoveryRestUrl'])
873 if api_data is None:
874 raise APIError('Failed to discover %s on %s' % (api['id'], host))
875 out[api['id']] = api_data
876 return out
877
878
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500879### Commands.
880
881
882def abort_task(_swarming, _manifest):
883 """Given a task manifest that was triggered, aborts its execution."""
884 # TODO(vadimsh): No supported by the server yet.
885
886
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400887def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400888 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500889 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500890 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500891 dest='dimensions', metavar='FOO bar',
892 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500893 parser.add_option_group(parser.filter_group)
894
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400895
Vadim Shtayurab450c602014-05-12 19:23:25 -0700896def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400897 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700898 parser.sharding_group.add_option(
899 '--shards', type='int', default=1,
900 help='Number of shards to trigger and collect.')
901 parser.add_option_group(parser.sharding_group)
902
903
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400904def add_trigger_options(parser):
905 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500906 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400907 add_filter_options(parser)
908
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400909 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500910 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500911 '-s', '--isolated',
912 help='Hash of the .isolated to grab from the isolate server')
913 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500914 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700915 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500916 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500917 '--priority', type='int', default=100,
918 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500919 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500920 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400921 help='Display name of the task. Defaults to '
922 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
923 'isolated file is provided, if a hash is provided, it defaults to '
924 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400925 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400926 '--tags', action='append', default=[],
927 help='Tags to assign to the task.')
928 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500929 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400930 help='User associated with the task. Defaults to authenticated user on '
931 'the server.')
932 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400933 '--idempotent', action='store_true', default=False,
934 help='When set, the server will actively try to find a previous task '
935 'with the same parameter and return this result instead if possible')
936 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400937 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400938 help='Seconds to allow the task to be pending for a bot to run before '
939 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400940 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400941 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400942 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400943 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400944 '--hard-timeout', type='int', default=60*60,
945 help='Seconds to allow the task to complete.')
946 parser.task_group.add_option(
947 '--io-timeout', type='int', default=20*60,
948 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500949 parser.task_group.add_option(
950 '--raw-cmd', action='store_true', default=False,
951 help='When set, the command after -- is used as-is without run_isolated. '
952 'In this case, no .isolated file is expected.')
borenet02f772b2016-06-22 12:42:19 -0700953 parser.task_group.add_option(
954 '--cipd-package', action='append', default=[],
955 help='CIPD packages to install on the Swarming bot. Uses the format: '
956 'path:package_name:version')
vadimsh93d167c2016-09-13 11:31:51 -0700957 parser.task_group.add_option(
958 '--service-account',
959 help='Name of a service account to run the task as. Only literal "bot" '
960 'string can be specified currently (to run the task under bot\'s '
961 'account). Don\'t use task service accounts if not given '
962 '(default).')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500963 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000964
965
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500966def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700967 """Processes trigger options and does preparatory steps.
968
969 Uploads files to isolate server and generates service account tokens if
970 necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500971 """
972 options.dimensions = dict(options.dimensions)
973 options.env = dict(options.env)
974
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500975 if not options.dimensions:
976 parser.error('Please at least specify one --dimension')
977 if options.raw_cmd:
978 if not args:
979 parser.error(
980 'Arguments with --raw-cmd should be passed after -- as command '
981 'delimiter.')
982 if options.isolate_server:
983 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
984
985 command = args
986 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500987 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500988 options.user,
989 '_'.join(
990 '%s=%s' % (k, v)
991 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700992 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500993 else:
nodir55be77b2016-05-03 09:39:57 -0700994 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500995 try:
maruel77f720b2015-09-15 12:35:22 -0700996 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500997 except ValueError as e:
998 parser.error(str(e))
999
borenet02f772b2016-06-22 12:42:19 -07001000 cipd_packages = []
1001 for p in options.cipd_package:
1002 split = p.split(':', 2)
1003 if len(split) != 3:
1004 parser.error('CIPD packages must take the form: path:package:version')
1005 cipd_packages.append(CipdPackage(
1006 package_name=split[1],
1007 path=split[0],
1008 version=split[2]))
1009 cipd_input = None
1010 if cipd_packages:
1011 cipd_input = CipdInput(
1012 client_package=None,
1013 packages=cipd_packages,
1014 server=None)
1015
nodir152cba62016-05-12 16:08:56 -07001016 # If inputs_ref.isolated is used, command is actually extra_args.
1017 # Otherwise it's an actual command to run.
1018 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -07001019 properties = TaskProperties(
borenet02f772b2016-06-22 12:42:19 -07001020 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -07001021 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001022 dimensions=options.dimensions,
1023 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001024 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -07001025 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -07001026 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001027 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001028 inputs_ref=inputs_ref,
1029 io_timeout_secs=options.io_timeout)
maruel8fce7962015-10-21 11:17:47 -07001030 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1031 parser.error('--tags must be in the format key:value')
vadimsh93d167c2016-09-13 11:31:51 -07001032
1033 # Convert a service account email to a signed service account token to pass
1034 # to Swarming.
1035 service_account_token = None
1036 if options.service_account in ('bot', 'none'):
1037 service_account_token = options.service_account
1038 elif options.service_account:
1039 # pylint: disable=assignment-from-no-return
1040 service_account_token = mint_service_account_token(options.service_account)
1041
maruel77f720b2015-09-15 12:35:22 -07001042 return NewTaskRequest(
1043 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001044 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001045 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001046 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001047 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001048 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001049 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001050 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001051
1052
1053def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001054 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001055 '-t', '--timeout', type='float',
1056 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1057 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001058 parser.group_logging.add_option(
1059 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001060 parser.group_logging.add_option(
1061 '--print-status-updates', action='store_true',
1062 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001063 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001064 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001065 '--task-summary-json',
1066 metavar='FILE',
1067 help='Dump a summary of task results to this file as json. It contains '
1068 'only shards statuses as know to server directly. Any output files '
1069 'emitted by the task can be collected by using --task-output-dir')
1070 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001071 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001072 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001073 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001074 'directory contains per-shard directory with output files produced '
1075 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001076 parser.task_output_group.add_option(
1077 '--perf', action='store_true', default=False,
1078 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001079 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001080
1081
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001082@subcommand.usage('bots...')
1083def CMDbot_delete(parser, args):
1084 """Forcibly deletes bots from the Swarming server."""
1085 parser.add_option(
1086 '-f', '--force', action='store_true',
1087 help='Do not prompt for confirmation')
1088 options, args = parser.parse_args(args)
1089 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001090 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001091
1092 bots = sorted(args)
1093 if not options.force:
1094 print('Delete the following bots?')
1095 for bot in bots:
1096 print(' %s' % bot)
1097 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1098 print('Goodbye.')
1099 return 1
1100
1101 result = 0
1102 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001103 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001104 if net.url_read_json(url, data={}, method='POST') is None:
1105 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001106 result = 1
1107 return result
1108
1109
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001110def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001111 """Returns information about the bots connected to the Swarming server."""
1112 add_filter_options(parser)
1113 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001114 '--dead-only', action='store_true',
1115 help='Only print dead bots, useful to reap them and reimage broken bots')
1116 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001117 '-k', '--keep-dead', action='store_true',
1118 help='Do not filter out dead bots')
1119 parser.filter_group.add_option(
1120 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001121 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001122 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001123
1124 if options.keep_dead and options.dead_only:
1125 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001126
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001127 bots = []
1128 cursor = None
1129 limit = 250
1130 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001131 base_url = (
maruel380e3262016-08-31 16:10:06 -07001132 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001133 while True:
1134 url = base_url
1135 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001136 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001137 data = net.url_read_json(url)
1138 if data is None:
1139 print >> sys.stderr, 'Failed to access %s' % options.swarming
1140 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001141 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001142 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001143 if not cursor:
1144 break
1145
maruel77f720b2015-09-15 12:35:22 -07001146 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001147 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001148 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001149 continue
maruel77f720b2015-09-15 12:35:22 -07001150 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001151 continue
1152
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001153 # If the user requested to filter on dimensions, ensure the bot has all the
1154 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001155 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001156 for key, value in options.dimensions:
1157 if key not in dimensions:
1158 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001159 # A bot can have multiple value for a key, for example,
1160 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1161 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001162 if isinstance(dimensions[key], list):
1163 if value not in dimensions[key]:
1164 break
1165 else:
1166 if value != dimensions[key]:
1167 break
1168 else:
maruel77f720b2015-09-15 12:35:22 -07001169 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001170 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001171 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001172 if bot.get('task_id'):
1173 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001174 return 0
1175
1176
maruelfd0a90c2016-06-10 11:51:10 -07001177@subcommand.usage('task_id')
1178def CMDcancel(parser, args):
1179 """Cancels a task."""
1180 options, args = parser.parse_args(args)
1181 if not args:
1182 parser.error('Please specify the task to cancel')
1183 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001184 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001185 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1186 print('Deleting %s failed. Probably already gone' % task_id)
1187 return 1
1188 return 0
1189
1190
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001191@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001192def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001193 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001194
1195 The result can be in multiple part if the execution was sharded. It can
1196 potentially have retries.
1197 """
1198 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001199 parser.add_option(
1200 '-j', '--json',
1201 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001202 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001203 if not args and not options.json:
1204 parser.error('Must specify at least one task id or --json.')
1205 if args and options.json:
1206 parser.error('Only use one of task id or --json.')
1207
1208 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001209 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001210 try:
maruel1ceb3872015-10-14 06:10:44 -07001211 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001212 data = json.load(f)
1213 except (IOError, ValueError):
1214 parser.error('Failed to open %s' % options.json)
1215 try:
1216 tasks = sorted(
1217 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1218 args = [t['task_id'] for t in tasks]
1219 except (KeyError, TypeError):
1220 parser.error('Failed to process %s' % options.json)
1221 if options.timeout is None:
1222 options.timeout = (
1223 data['request']['properties']['execution_timeout_secs'] +
1224 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001225 else:
1226 valid = frozenset('0123456789abcdef')
1227 if any(not valid.issuperset(task_id) for task_id in args):
1228 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001229
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001230 try:
1231 return collect(
1232 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001233 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001234 options.timeout,
1235 options.decorate,
1236 options.print_status_updates,
1237 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001238 options.task_output_dir,
1239 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001240 except Failure:
1241 on_error.report(None)
1242 return 1
1243
1244
maruelbea00862015-09-18 09:55:36 -07001245@subcommand.usage('[filename]')
1246def CMDput_bootstrap(parser, args):
1247 """Uploads a new version of bootstrap.py."""
1248 options, args = parser.parse_args(args)
1249 if len(args) != 1:
1250 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001251 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001252 path = unicode(os.path.abspath(args[0]))
1253 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001254 content = f.read().decode('utf-8')
1255 data = net.url_read_json(url, data={'content': content})
1256 print data
1257 return 0
1258
1259
1260@subcommand.usage('[filename]')
1261def CMDput_bot_config(parser, args):
1262 """Uploads a new version of bot_config.py."""
1263 options, args = parser.parse_args(args)
1264 if len(args) != 1:
1265 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001266 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001267 path = unicode(os.path.abspath(args[0]))
1268 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001269 content = f.read().decode('utf-8')
1270 data = net.url_read_json(url, data={'content': content})
1271 print data
1272 return 0
1273
1274
maruel77f720b2015-09-15 12:35:22 -07001275@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001276def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001277 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1278 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001279
1280 Examples:
maruel77f720b2015-09-15 12:35:22 -07001281 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001282 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001283
maruel77f720b2015-09-15 12:35:22 -07001284 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001285 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1286
1287 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1288 quoting is important!:
1289 swarming.py query -S server-url.com --limit 10 \\
1290 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001291 """
1292 CHUNK_SIZE = 250
1293
1294 parser.add_option(
1295 '-L', '--limit', type='int', default=200,
1296 help='Limit to enforce on limitless items (like number of tasks); '
1297 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001298 parser.add_option(
1299 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001300 parser.add_option(
1301 '--progress', action='store_true',
1302 help='Prints a dot at each request to show progress')
1303 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001304 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001305 parser.error(
1306 'Must specify only method name and optionally query args properly '
1307 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001308 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001309 url = base_url
1310 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001311 # Check check, change if not working out.
1312 merge_char = '&' if '?' in url else '?'
1313 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001314 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001315 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001316 # TODO(maruel): Do basic diagnostic.
1317 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001318 return 1
1319
1320 # Some items support cursors. Try to get automatically if cursors are needed
1321 # by looking at the 'cursor' items.
1322 while (
1323 data.get('cursor') and
1324 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001325 merge_char = '&' if '?' in base_url else '?'
1326 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001327 if options.limit:
1328 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001329 if options.progress:
1330 sys.stdout.write('.')
1331 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001332 new = net.url_read_json(url)
1333 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001334 if options.progress:
1335 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001336 print >> sys.stderr, 'Failed to access %s' % options.swarming
1337 return 1
maruel81b37132015-10-21 06:42:13 -07001338 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001339 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001340
maruel77f720b2015-09-15 12:35:22 -07001341 if options.progress:
1342 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001343 if options.limit and len(data.get('items', [])) > options.limit:
1344 data['items'] = data['items'][:options.limit]
1345 data.pop('cursor', None)
1346
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001347 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001348 options.json = unicode(os.path.abspath(options.json))
1349 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001350 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001351 try:
maruel77f720b2015-09-15 12:35:22 -07001352 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001353 sys.stdout.write('\n')
1354 except IOError:
1355 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001356 return 0
1357
1358
maruel77f720b2015-09-15 12:35:22 -07001359def CMDquery_list(parser, args):
1360 """Returns list of all the Swarming APIs that can be used with command
1361 'query'.
1362 """
1363 parser.add_option(
1364 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1365 options, args = parser.parse_args(args)
1366 if args:
1367 parser.error('No argument allowed.')
1368
1369 try:
1370 apis = endpoints_api_discovery_apis(options.swarming)
1371 except APIError as e:
1372 parser.error(str(e))
1373 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001374 options.json = unicode(os.path.abspath(options.json))
1375 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001376 json.dump(apis, f)
1377 else:
1378 help_url = (
1379 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1380 options.swarming)
1381 for api_id, api in sorted(apis.iteritems()):
1382 print api_id
1383 print ' ' + api['description']
1384 for resource_name, resource in sorted(api['resources'].iteritems()):
1385 print ''
1386 for method_name, method in sorted(resource['methods'].iteritems()):
1387 # Only list the GET ones.
1388 if method['httpMethod'] != 'GET':
1389 continue
1390 print '- %s.%s: %s' % (
1391 resource_name, method_name, method['path'])
1392 print ' ' + method['description']
1393 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1394 return 0
1395
1396
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001397@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001398def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001399 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001400
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001401 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001402 """
1403 add_trigger_options(parser)
1404 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001405 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001406 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001407 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001408 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001409 tasks = trigger_task_shards(
1410 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001411 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001412 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001413 'Failed to trigger %s(%s): %s' %
1414 (options.task_name, args[0], e.args[0]))
1415 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001416 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001417 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001418 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001419 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001420 task_ids = [
1421 t['task_id']
1422 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1423 ]
maruel71c61c82016-02-22 06:52:05 -08001424 if options.timeout is None:
1425 options.timeout = (
1426 task_request.properties.execution_timeout_secs +
1427 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001428 try:
1429 return collect(
1430 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001431 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001432 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001433 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001434 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001435 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001436 options.task_output_dir,
1437 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001438 except Failure:
1439 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001440 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001441
1442
maruel18122c62015-10-23 06:31:23 -07001443@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001444def CMDreproduce(parser, args):
1445 """Runs a task locally that was triggered on the server.
1446
1447 This running locally the same commands that have been run on the bot. The data
1448 downloaded will be in a subdirectory named 'work' of the current working
1449 directory.
maruel18122c62015-10-23 06:31:23 -07001450
1451 You can pass further additional arguments to the target command by passing
1452 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001453 """
maruelc070e672016-02-22 17:32:57 -08001454 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001455 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001456 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001457 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001458 extra_args = []
1459 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001460 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001461 if len(args) > 1:
1462 if args[1] == '--':
1463 if len(args) > 2:
1464 extra_args = args[2:]
1465 else:
1466 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001467
maruel380e3262016-08-31 16:10:06 -07001468 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001469 request = net.url_read_json(url)
1470 if not request:
1471 print >> sys.stderr, 'Failed to retrieve request data for the task'
1472 return 1
1473
maruel12e30012015-10-09 11:55:35 -07001474 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001475 if fs.isdir(workdir):
1476 parser.error('Please delete the directory \'work\' first')
1477 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001478
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001479 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001480 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001481 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001482 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001483 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001484 for i in properties['env']:
1485 key = i['key'].encode('utf-8')
1486 if not i['value']:
1487 env.pop(key, None)
1488 else:
1489 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001490
nodir152cba62016-05-12 16:08:56 -07001491 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001492 # Create the tree.
1493 with isolateserver.get_storage(
1494 properties['inputs_ref']['isolatedserver'],
1495 properties['inputs_ref']['namespace']) as storage:
1496 bundle = isolateserver.fetch_isolated(
1497 properties['inputs_ref']['isolated'],
1498 storage,
1499 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001500 workdir,
1501 False)
maruel29ab2fd2015-10-16 11:44:01 -07001502 command = bundle.command
1503 if bundle.relative_cwd:
1504 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001505 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001506 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001507 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001508 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001509 if not options.output_dir and new_command != command:
1510 parser.error('The task has outputs, you must use --output-dir')
1511 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001512 else:
1513 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001514 try:
maruel18122c62015-10-23 06:31:23 -07001515 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001516 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001517 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001518 print >> sys.stderr, str(e)
1519 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001520
1521
maruel0eb1d1b2015-10-02 14:48:21 -07001522@subcommand.usage('bot_id')
1523def CMDterminate(parser, args):
1524 """Tells a bot to gracefully shut itself down as soon as it can.
1525
1526 This is done by completing whatever current task there is then exiting the bot
1527 process.
1528 """
1529 parser.add_option(
1530 '--wait', action='store_true', help='Wait for the bot to terminate')
1531 options, args = parser.parse_args(args)
1532 if len(args) != 1:
1533 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001534 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001535 request = net.url_read_json(url, data={})
1536 if not request:
1537 print >> sys.stderr, 'Failed to ask for termination'
1538 return 1
1539 if options.wait:
1540 return collect(
maruel9531ce02016-04-13 06:11:23 -07001541 options.swarming, [request['task_id']], 0., False, False, None, None,
1542 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001543 return 0
1544
1545
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001546@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001547def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001548 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001549
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001550 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001551 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001552
1553 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001554
1555 Passes all extra arguments provided after '--' as additional command line
1556 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001557 """
1558 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001559 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001560 parser.add_option(
1561 '--dump-json',
1562 metavar='FILE',
1563 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001564 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001565 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001566 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001567 tasks = trigger_task_shards(
1568 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001569 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001570 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001571 tasks_sorted = sorted(
1572 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001573 if options.dump_json:
1574 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001575 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001576 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001577 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001578 }
maruel46b015f2015-10-13 18:40:35 -07001579 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001580 print('To collect results, use:')
1581 print(' swarming.py collect -S %s --json %s' %
1582 (options.swarming, options.dump_json))
1583 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001584 print('To collect results, use:')
1585 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001586 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1587 print('Or visit:')
1588 for t in tasks_sorted:
1589 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001590 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001591 except Failure:
1592 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001593 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001594
1595
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001596class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001597 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001598 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001599 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001600 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001601 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001602 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001603 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001604 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001605 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001606 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001607
1608 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001609 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001610 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001611 auth.process_auth_options(self, options)
1612 user = self._process_swarming(options)
1613 if hasattr(options, 'user') and not options.user:
1614 options.user = user
1615 return options, args
1616
1617 def _process_swarming(self, options):
1618 """Processes the --swarming option and aborts if not specified.
1619
1620 Returns the identity as determined by the server.
1621 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001622 if not options.swarming:
1623 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001624 try:
1625 options.swarming = net.fix_url(options.swarming)
1626 except ValueError as e:
1627 self.error('--swarming %s' % e)
1628 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001629 try:
1630 user = auth.ensure_logged_in(options.swarming)
1631 except ValueError as e:
1632 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001633 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001634
1635
1636def main(args):
1637 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001638 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001639
1640
1641if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001642 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001643 fix_encoding.fix_encoding()
1644 tools.disable_buffering()
1645 colorama.init()
1646 sys.exit(main(sys.argv[1:]))