blob: b147c34d363ab4a2437ba79a0dc07f98f83dd6f1 [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
aludwincc5524e2016-10-28 10:25:24 -07008__version__ = '0.8.8'
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',
aludwincc5524e2016-10-28 10:25:24 -0700178 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700179 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700180 ])
181
182
183# See ../appengine/swarming/swarming_rpcs.py.
184NewTaskRequest = collections.namedtuple(
185 'NewTaskRequest',
186 [
187 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500188 'name',
maruel77f720b2015-09-15 12:35:22 -0700189 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500190 'priority',
maruel77f720b2015-09-15 12:35:22 -0700191 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700192 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500193 'tags',
194 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500195 ])
196
197
maruel77f720b2015-09-15 12:35:22 -0700198def namedtuple_to_dict(value):
199 """Recursively converts a namedtuple to a dict."""
200 out = dict(value._asdict())
201 for k, v in out.iteritems():
202 if hasattr(v, '_asdict'):
203 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700204 elif isinstance(v, (list, tuple)):
205 l = []
206 for elem in v:
207 if hasattr(elem, '_asdict'):
208 l.append(namedtuple_to_dict(elem))
209 else:
210 l.append(elem)
211 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700212 return out
213
214
vadimsh93d167c2016-09-13 11:31:51 -0700215def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800216 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700217
218 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500219 """
maruel77f720b2015-09-15 12:35:22 -0700220 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700221 if hide_token:
222 if out['service_account_token'] not in (None, 'bot', 'none'):
223 out['service_account_token'] = '<hidden>'
224 # Don't send 'service_account_token' if it is None to avoid confusing older
225 # version of the server that doesn't know about 'service_account_token'.
226 if out['service_account_token'] in (None, 'none'):
227 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700228 # Maps are not supported until protobuf v3.
229 out['properties']['dimensions'] = [
230 {'key': k, 'value': v}
231 for k, v in out['properties']['dimensions'].iteritems()
232 ]
233 out['properties']['dimensions'].sort(key=lambda x: x['key'])
234 out['properties']['env'] = [
235 {'key': k, 'value': v}
236 for k, v in out['properties']['env'].iteritems()
237 ]
238 out['properties']['env'].sort(key=lambda x: x['key'])
239 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500240
241
maruel77f720b2015-09-15 12:35:22 -0700242def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500243 """Triggers a request on the Swarming server and returns the json data.
244
245 It's the low-level function.
246
247 Returns:
248 {
249 'request': {
250 'created_ts': u'2010-01-02 03:04:05',
251 'name': ..
252 },
253 'task_id': '12300',
254 }
255 """
256 logging.info('Triggering: %s', raw_request['name'])
257
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500258 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700259 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500260 if not result:
261 on_error.report('Failed to trigger task %s' % raw_request['name'])
262 return None
maruele557bce2015-11-17 09:01:27 -0800263 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800264 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800265 msg = 'Failed to trigger task %s' % raw_request['name']
266 if result['error'].get('errors'):
267 for err in result['error']['errors']:
268 if err.get('message'):
269 msg += '\nMessage: %s' % err['message']
270 if err.get('debugInfo'):
271 msg += '\nDebug info:\n%s' % err['debugInfo']
272 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800273 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800274
275 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800276 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500277 return result
278
279
280def setup_googletest(env, shards, index):
281 """Sets googletest specific environment variables."""
282 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700283 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
284 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
285 env = env[:]
286 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
287 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500288 return env
289
290
291def trigger_task_shards(swarming, task_request, shards):
292 """Triggers one or many subtasks of a sharded task.
293
294 Returns:
295 Dict with task details, returned to caller as part of --dump-json output.
296 None in case of failure.
297 """
298 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700299 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500300 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700301 req['properties']['env'] = setup_googletest(
302 req['properties']['env'], shards, index)
303 req['name'] += ':%s:%s' % (index, shards)
304 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500305
306 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500307 tasks = {}
308 priority_warning = False
309 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700310 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500311 if not task:
312 break
313 logging.info('Request result: %s', task)
314 if (not priority_warning and
315 task['request']['priority'] != task_request.priority):
316 priority_warning = True
317 print >> sys.stderr, (
318 'Priority was reset to %s' % task['request']['priority'])
319 tasks[request['name']] = {
320 'shard_index': index,
321 'task_id': task['task_id'],
322 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
323 }
324
325 # Some shards weren't triggered. Abort everything.
326 if len(tasks) != len(requests):
327 if tasks:
328 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
329 len(tasks), len(requests))
330 for task_dict in tasks.itervalues():
331 abort_task(swarming, task_dict['task_id'])
332 return None
333
334 return tasks
335
336
vadimsh93d167c2016-09-13 11:31:51 -0700337def mint_service_account_token(service_account):
338 """Given a service account name returns a delegation token for this account.
339
340 The token is generated based on triggering user's credentials. It is passed
341 to Swarming, that uses it when running tasks.
342 """
343 logging.info(
344 'Generating delegation token for service account "%s"', service_account)
345 raise NotImplementedError('Custom service accounts are not implemented yet')
346
347
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500348### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000349
350
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700351# How often to print status updates to stdout in 'collect'.
352STATUS_UPDATE_INTERVAL = 15 * 60.
353
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400354
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400355class State(object):
356 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000357
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400358 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
359 values are part of the API so if they change, the API changed.
360
361 It's in fact an enum. Values should be in decreasing order of importance.
362 """
363 RUNNING = 0x10
364 PENDING = 0x20
365 EXPIRED = 0x30
366 TIMED_OUT = 0x40
367 BOT_DIED = 0x50
368 CANCELED = 0x60
369 COMPLETED = 0x70
370
maruel77f720b2015-09-15 12:35:22 -0700371 STATES = (
372 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
373 'COMPLETED')
374 STATES_RUNNING = ('RUNNING', 'PENDING')
375 STATES_NOT_RUNNING = (
376 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
377 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
378 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400379
380 _NAMES = {
381 RUNNING: 'Running',
382 PENDING: 'Pending',
383 EXPIRED: 'Expired',
384 TIMED_OUT: 'Execution timed out',
385 BOT_DIED: 'Bot died',
386 CANCELED: 'User canceled',
387 COMPLETED: 'Completed',
388 }
389
maruel77f720b2015-09-15 12:35:22 -0700390 _ENUMS = {
391 'RUNNING': RUNNING,
392 'PENDING': PENDING,
393 'EXPIRED': EXPIRED,
394 'TIMED_OUT': TIMED_OUT,
395 'BOT_DIED': BOT_DIED,
396 'CANCELED': CANCELED,
397 'COMPLETED': COMPLETED,
398 }
399
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400400 @classmethod
401 def to_string(cls, state):
402 """Returns a user-readable string representing a State."""
403 if state not in cls._NAMES:
404 raise ValueError('Invalid state %s' % state)
405 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000406
maruel77f720b2015-09-15 12:35:22 -0700407 @classmethod
408 def from_enum(cls, state):
409 """Returns int value based on the string."""
410 if state not in cls._ENUMS:
411 raise ValueError('Invalid state %s' % state)
412 return cls._ENUMS[state]
413
maruel@chromium.org0437a732013-08-27 16:05:52 +0000414
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700415class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700416 """Assembles task execution summary (for --task-summary-json output).
417
418 Optionally fetches task outputs from isolate server to local disk (used when
419 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700420
421 This object is shared among multiple threads running 'retrieve_results'
422 function, in particular they call 'process_shard_result' method in parallel.
423 """
424
maruel0eb1d1b2015-10-02 14:48:21 -0700425 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700426 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
427
428 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700429 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430 shard_count: expected number of task shards.
431 """
maruel12e30012015-10-09 11:55:35 -0700432 self.task_output_dir = (
433 unicode(os.path.abspath(task_output_dir))
434 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700435 self.shard_count = shard_count
436
437 self._lock = threading.Lock()
438 self._per_shard_results = {}
439 self._storage = None
440
nodire5028a92016-04-29 14:38:21 -0700441 if self.task_output_dir:
442 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443
Vadim Shtayurab450c602014-05-12 19:23:25 -0700444 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 """Stores results of a single task shard, fetches output files if necessary.
446
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400447 Modifies |result| in place.
448
maruel77f720b2015-09-15 12:35:22 -0700449 shard_index is 0-based.
450
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700451 Called concurrently from multiple threads.
452 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700453 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700454 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700455 if shard_index < 0 or shard_index >= self.shard_count:
456 logging.warning(
457 'Shard index %d is outside of expected range: [0; %d]',
458 shard_index, self.shard_count - 1)
459 return
460
maruel77f720b2015-09-15 12:35:22 -0700461 if result.get('outputs_ref'):
462 ref = result['outputs_ref']
463 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
464 ref['isolatedserver'],
465 urllib.urlencode(
466 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400467
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700468 # Store result dict of that shard, ignore results we've already seen.
469 with self._lock:
470 if shard_index in self._per_shard_results:
471 logging.warning('Ignoring duplicate shard index %d', shard_index)
472 return
473 self._per_shard_results[shard_index] = result
474
475 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700476 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400477 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700478 result['outputs_ref']['isolatedserver'],
479 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400480 if storage:
481 # Output files are supposed to be small and they are not reused across
482 # tasks. So use MemoryCache for them instead of on-disk cache. Make
483 # files writable, so that calling script can delete them.
484 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700485 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400486 storage,
487 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700488 os.path.join(self.task_output_dir, str(shard_index)),
489 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700490
491 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700492 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700493 with self._lock:
494 # Write an array of shard results with None for missing shards.
495 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700496 'shards': [
497 self._per_shard_results.get(i) for i in xrange(self.shard_count)
498 ],
499 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700500 # Write summary.json to task_output_dir as well.
501 if self.task_output_dir:
502 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700503 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700504 summary,
505 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700506 if self._storage:
507 self._storage.close()
508 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700509 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700510
511 def _get_storage(self, isolate_server, namespace):
512 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700513 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700514 with self._lock:
515 if not self._storage:
516 self._storage = isolateserver.get_storage(isolate_server, namespace)
517 else:
518 # Shards must all use exact same isolate server and namespace.
519 if self._storage.location != isolate_server:
520 logging.error(
521 'Task shards are using multiple isolate servers: %s and %s',
522 self._storage.location, isolate_server)
523 return None
524 if self._storage.namespace != namespace:
525 logging.error(
526 'Task shards are using multiple namespaces: %s and %s',
527 self._storage.namespace, namespace)
528 return None
529 return self._storage
530
531
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500532def now():
533 """Exists so it can be mocked easily."""
534 return time.time()
535
536
maruel77f720b2015-09-15 12:35:22 -0700537def parse_time(value):
538 """Converts serialized time from the API to datetime.datetime."""
539 # When microseconds are 0, the '.123456' suffix is elided. This means the
540 # serialized format is not consistent, which confuses the hell out of python.
541 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
542 try:
543 return datetime.datetime.strptime(value, fmt)
544 except ValueError:
545 pass
546 raise ValueError('Failed to parse %s' % value)
547
548
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700549def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700550 base_url, shard_index, task_id, timeout, should_stop, output_collector,
551 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400552 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700553
Vadim Shtayurab450c602014-05-12 19:23:25 -0700554 Returns:
555 <result dict> on success.
556 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700557 """
maruel71c61c82016-02-22 06:52:05 -0800558 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700559 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700560 if include_perf:
561 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700562 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700563 started = now()
564 deadline = started + timeout if timeout else None
565 attempt = 0
566
567 while not should_stop.is_set():
568 attempt += 1
569
570 # Waiting for too long -> give up.
571 current_time = now()
572 if deadline and current_time >= deadline:
573 logging.error('retrieve_results(%s) timed out on attempt %d',
574 base_url, attempt)
575 return None
576
577 # Do not spin too fast. Spin faster at the beginning though.
578 # Start with 1 sec delay and for each 30 sec of waiting add another second
579 # of delay, until hitting 15 sec ceiling.
580 if attempt > 1:
581 max_delay = min(15, 1 + (current_time - started) / 30.0)
582 delay = min(max_delay, deadline - current_time) if deadline else max_delay
583 if delay > 0:
584 logging.debug('Waiting %.1f sec before retrying', delay)
585 should_stop.wait(delay)
586 if should_stop.is_set():
587 return None
588
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400589 # Disable internal retries in net.url_read_json, since we are doing retries
590 # ourselves.
591 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700592 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
593 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400594 result = net.url_read_json(result_url, retry_50x=False)
595 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400596 continue
maruel77f720b2015-09-15 12:35:22 -0700597
maruelbf53e042015-12-01 15:00:51 -0800598 if result.get('error'):
599 # An error occurred.
600 if result['error'].get('errors'):
601 for err in result['error']['errors']:
602 logging.warning(
603 'Error while reading task: %s; %s',
604 err.get('message'), err.get('debugInfo'))
605 elif result['error'].get('message'):
606 logging.warning(
607 'Error while reading task: %s', result['error']['message'])
608 continue
609
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400610 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700611 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400612 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700613 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700614 # Record the result, try to fetch attached output files (if any).
615 if output_collector:
616 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700617 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700618 if result.get('internal_failure'):
619 logging.error('Internal error!')
620 elif result['state'] == 'BOT_DIED':
621 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700622 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000623
624
maruel77f720b2015-09-15 12:35:22 -0700625def convert_to_old_format(result):
626 """Converts the task result data from Endpoints API format to old API format
627 for compatibility.
628
629 This goes into the file generated as --task-summary-json.
630 """
631 # Sets default.
632 result.setdefault('abandoned_ts', None)
633 result.setdefault('bot_id', None)
634 result.setdefault('bot_version', None)
635 result.setdefault('children_task_ids', [])
636 result.setdefault('completed_ts', None)
637 result.setdefault('cost_saved_usd', None)
638 result.setdefault('costs_usd', None)
639 result.setdefault('deduped_from', None)
640 result.setdefault('name', None)
641 result.setdefault('outputs_ref', None)
642 result.setdefault('properties_hash', None)
643 result.setdefault('server_versions', None)
644 result.setdefault('started_ts', None)
645 result.setdefault('tags', None)
646 result.setdefault('user', None)
647
648 # Convertion back to old API.
649 duration = result.pop('duration', None)
650 result['durations'] = [duration] if duration else []
651 exit_code = result.pop('exit_code', None)
652 result['exit_codes'] = [int(exit_code)] if exit_code else []
653 result['id'] = result.pop('task_id')
654 result['isolated_out'] = result.get('outputs_ref', None)
655 output = result.pop('output', None)
656 result['outputs'] = [output] if output else []
657 # properties_hash
658 # server_version
659 # Endpoints result 'state' as string. For compatibility with old code, convert
660 # to int.
661 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700662 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700663 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700664 if 'bot_dimensions' in result:
665 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700666 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700667 }
668 else:
669 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700670
671
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700672def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400673 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700674 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500675 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000676
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700677 Duplicate shards are ignored. Shards are yielded in order of completion.
678 Timed out shards are NOT yielded at all. Caller can compare number of yielded
679 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000680
681 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500682 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 +0000683 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500684
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700685 output_collector is an optional instance of TaskOutputCollector that will be
686 used to fetch files produced by a task from isolate server to the local disk.
687
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500688 Yields:
689 (index, result). In particular, 'result' is defined as the
690 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000691 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000692 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400693 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700694 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700695 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700696
maruel@chromium.org0437a732013-08-27 16:05:52 +0000697 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
698 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700699 # Adds a task to the thread pool to call 'retrieve_results' and return
700 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400701 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700702 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000703 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400704 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700705 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700706
707 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400708 for shard_index, task_id in enumerate(task_ids):
709 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700710
711 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400712 shards_remaining = range(len(task_ids))
713 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700714 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700715 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700716 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700717 shard_index, result = results_channel.pull(
718 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700719 except threading_utils.TaskChannel.Timeout:
720 if print_status_updates:
721 print(
722 'Waiting for results from the following shards: %s' %
723 ', '.join(map(str, shards_remaining)))
724 sys.stdout.flush()
725 continue
726 except Exception:
727 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700728
729 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700730 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000731 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500732 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000733 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700734
Vadim Shtayurab450c602014-05-12 19:23:25 -0700735 # Yield back results to the caller.
736 assert shard_index in shards_remaining
737 shards_remaining.remove(shard_index)
738 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700739
maruel@chromium.org0437a732013-08-27 16:05:52 +0000740 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700741 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000742 should_stop.set()
743
744
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400745def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000746 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700747 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400748 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700749 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
750 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400751 else:
752 pending = 'N/A'
753
maruel77f720b2015-09-15 12:35:22 -0700754 if metadata.get('duration') is not None:
755 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400756 else:
757 duration = 'N/A'
758
maruel77f720b2015-09-15 12:35:22 -0700759 if metadata.get('exit_code') is not None:
760 # Integers are encoded as string to not loose precision.
761 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400762 else:
763 exit_code = 'N/A'
764
765 bot_id = metadata.get('bot_id') or 'N/A'
766
maruel77f720b2015-09-15 12:35:22 -0700767 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400768 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400769 tag_footer = (
770 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
771 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400772
773 tag_len = max(len(tag_header), len(tag_footer))
774 dash_pad = '+-%s-+\n' % ('-' * tag_len)
775 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
776 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
777
778 header = dash_pad + tag_header + dash_pad
779 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700780 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400781 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000782
783
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700784def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700785 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700786 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700787 """Retrieves results of a Swarming task.
788
789 Returns:
790 process exit code that should be returned to the user.
791 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700792 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700793 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700794
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700795 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700796 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400797 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700798 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400799 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400800 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700801 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700802 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700803
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400804 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700805 shard_exit_code = metadata.get('exit_code')
806 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700807 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700808 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700809 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400810 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700811 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700812
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700813 if decorate:
leileied181762016-10-13 14:24:59 -0700814 s = decorate_shard_output(swarming, index, metadata).encode(
815 'utf-8', 'replace')
816 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400817 if len(seen_shards) < len(task_ids):
818 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700819 else:
maruel77f720b2015-09-15 12:35:22 -0700820 print('%s: %s %s' % (
821 metadata.get('bot_id', 'N/A'),
822 metadata['task_id'],
823 shard_exit_code))
824 if metadata['output']:
825 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400826 if output:
827 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700828 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700829 summary = output_collector.finalize()
830 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700831 # TODO(maruel): Make this optional.
832 for i in summary['shards']:
833 if i:
834 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700835 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700836
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400837 if decorate and total_duration:
838 print('Total duration: %.1fs' % total_duration)
839
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400840 if len(seen_shards) != len(task_ids):
841 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700842 print >> sys.stderr, ('Results from some shards are missing: %s' %
843 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700844 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700845
maruela5490782015-09-30 10:56:59 -0700846 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000847
848
maruel77f720b2015-09-15 12:35:22 -0700849### API management.
850
851
852class APIError(Exception):
853 pass
854
855
856def endpoints_api_discovery_apis(host):
857 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
858 the APIs exposed by a host.
859
860 https://developers.google.com/discovery/v1/reference/apis/list
861 """
maruel380e3262016-08-31 16:10:06 -0700862 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
863 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700864 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
865 if data is None:
866 raise APIError('Failed to discover APIs on %s' % host)
867 out = {}
868 for api in data['items']:
869 if api['id'] == 'discovery:v1':
870 continue
871 # URL is of the following form:
872 # url = host + (
873 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
874 api_data = net.url_read_json(api['discoveryRestUrl'])
875 if api_data is None:
876 raise APIError('Failed to discover %s on %s' % (api['id'], host))
877 out[api['id']] = api_data
878 return out
879
880
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500881### Commands.
882
883
884def abort_task(_swarming, _manifest):
885 """Given a task manifest that was triggered, aborts its execution."""
886 # TODO(vadimsh): No supported by the server yet.
887
888
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400889def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400890 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500891 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500892 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500893 dest='dimensions', metavar='FOO bar',
894 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500895 parser.add_option_group(parser.filter_group)
896
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400897
Vadim Shtayurab450c602014-05-12 19:23:25 -0700898def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400899 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700900 parser.sharding_group.add_option(
901 '--shards', type='int', default=1,
902 help='Number of shards to trigger and collect.')
903 parser.add_option_group(parser.sharding_group)
904
905
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400906def add_trigger_options(parser):
907 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500908 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909 add_filter_options(parser)
910
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400911 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500912 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500913 '-s', '--isolated',
914 help='Hash of the .isolated to grab from the isolate server')
915 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500916 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700917 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500918 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500919 '--priority', type='int', default=100,
920 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500921 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500922 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400923 help='Display name of the task. Defaults to '
924 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
925 'isolated file is provided, if a hash is provided, it defaults to '
926 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400927 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400928 '--tags', action='append', default=[],
929 help='Tags to assign to the task.')
930 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500931 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400932 help='User associated with the task. Defaults to authenticated user on '
933 'the server.')
934 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400935 '--idempotent', action='store_true', default=False,
936 help='When set, the server will actively try to find a previous task '
937 'with the same parameter and return this result instead if possible')
938 parser.task_group.add_option(
iannuccieee1bca2016-10-28 13:16:23 -0700939 '--secret-bytes-path',
iannuccidc80dfb2016-10-28 12:50:20 -0700940 help='The optional path to a file containing the secret_bytes to use with'
941 'this task.')
942 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400943 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400944 help='Seconds to allow the task to be pending for a bot to run before '
945 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400946 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400947 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400948 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400949 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400950 '--hard-timeout', type='int', default=60*60,
951 help='Seconds to allow the task to complete.')
952 parser.task_group.add_option(
953 '--io-timeout', type='int', default=20*60,
954 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500955 parser.task_group.add_option(
956 '--raw-cmd', action='store_true', default=False,
957 help='When set, the command after -- is used as-is without run_isolated. '
958 'In this case, no .isolated file is expected.')
borenet02f772b2016-06-22 12:42:19 -0700959 parser.task_group.add_option(
960 '--cipd-package', action='append', default=[],
961 help='CIPD packages to install on the Swarming bot. Uses the format: '
962 'path:package_name:version')
vadimsh93d167c2016-09-13 11:31:51 -0700963 parser.task_group.add_option(
964 '--service-account',
965 help='Name of a service account to run the task as. Only literal "bot" '
966 'string can be specified currently (to run the task under bot\'s '
967 'account). Don\'t use task service accounts if not given '
968 '(default).')
aludwincc5524e2016-10-28 10:25:24 -0700969 parser.task_group.add_option(
970 '-o', '--output', action='append', default=[],
971 help='A list of files to return in addition to those written to'
972 '$(ISOLATED_OUTDIR). An error will occur if a file specified by'
973 'this option is also written directly to $(ISOLATED_OUTDIR).')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500974 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000975
976
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500977def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700978 """Processes trigger options and does preparatory steps.
979
980 Uploads files to isolate server and generates service account tokens if
981 necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500982 """
983 options.dimensions = dict(options.dimensions)
984 options.env = dict(options.env)
985
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500986 if not options.dimensions:
987 parser.error('Please at least specify one --dimension')
988 if options.raw_cmd:
989 if not args:
990 parser.error(
991 'Arguments with --raw-cmd should be passed after -- as command '
992 'delimiter.')
993 if options.isolate_server:
994 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
995
996 command = args
997 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500998 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500999 options.user,
1000 '_'.join(
1001 '%s=%s' % (k, v)
1002 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -07001003 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001004 else:
nodir55be77b2016-05-03 09:39:57 -07001005 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001006 try:
maruel77f720b2015-09-15 12:35:22 -07001007 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001008 except ValueError as e:
1009 parser.error(str(e))
1010
borenet02f772b2016-06-22 12:42:19 -07001011 cipd_packages = []
1012 for p in options.cipd_package:
1013 split = p.split(':', 2)
1014 if len(split) != 3:
1015 parser.error('CIPD packages must take the form: path:package:version')
1016 cipd_packages.append(CipdPackage(
1017 package_name=split[1],
1018 path=split[0],
1019 version=split[2]))
1020 cipd_input = None
1021 if cipd_packages:
1022 cipd_input = CipdInput(
1023 client_package=None,
1024 packages=cipd_packages,
1025 server=None)
1026
iannuccidc80dfb2016-10-28 12:50:20 -07001027 secret_bytes = None
1028 if options.secret_bytes_path:
1029 with open(options.secret_bytes_path, 'r') as f:
1030 secret_bytes = f.read().encode('base64')
1031
nodir152cba62016-05-12 16:08:56 -07001032 # If inputs_ref.isolated is used, command is actually extra_args.
1033 # Otherwise it's an actual command to run.
1034 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -07001035 properties = TaskProperties(
borenet02f772b2016-06-22 12:42:19 -07001036 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -07001037 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001038 dimensions=options.dimensions,
1039 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001040 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -07001041 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -07001042 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001043 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001044 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001045 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001046 outputs=options.output,
1047 secret_bytes=secret_bytes)
maruel8fce7962015-10-21 11:17:47 -07001048 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1049 parser.error('--tags must be in the format key:value')
vadimsh93d167c2016-09-13 11:31:51 -07001050
1051 # Convert a service account email to a signed service account token to pass
1052 # to Swarming.
1053 service_account_token = None
1054 if options.service_account in ('bot', 'none'):
1055 service_account_token = options.service_account
1056 elif options.service_account:
1057 # pylint: disable=assignment-from-no-return
1058 service_account_token = mint_service_account_token(options.service_account)
1059
maruel77f720b2015-09-15 12:35:22 -07001060 return NewTaskRequest(
1061 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001062 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001063 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001064 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001065 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001066 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001067 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001068 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001069
1070
1071def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001072 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001073 '-t', '--timeout', type='float',
1074 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1075 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001076 parser.group_logging.add_option(
1077 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001078 parser.group_logging.add_option(
1079 '--print-status-updates', action='store_true',
1080 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001081 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001082 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001083 '--task-summary-json',
1084 metavar='FILE',
1085 help='Dump a summary of task results to this file as json. It contains '
1086 'only shards statuses as know to server directly. Any output files '
1087 'emitted by the task can be collected by using --task-output-dir')
1088 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001089 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001090 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001091 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001092 'directory contains per-shard directory with output files produced '
1093 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001094 parser.task_output_group.add_option(
1095 '--perf', action='store_true', default=False,
1096 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001097 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001098
1099
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001100@subcommand.usage('bots...')
1101def CMDbot_delete(parser, args):
1102 """Forcibly deletes bots from the Swarming server."""
1103 parser.add_option(
1104 '-f', '--force', action='store_true',
1105 help='Do not prompt for confirmation')
1106 options, args = parser.parse_args(args)
1107 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001108 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001109
1110 bots = sorted(args)
1111 if not options.force:
1112 print('Delete the following bots?')
1113 for bot in bots:
1114 print(' %s' % bot)
1115 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1116 print('Goodbye.')
1117 return 1
1118
1119 result = 0
1120 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001121 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001122 if net.url_read_json(url, data={}, method='POST') is None:
1123 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001124 result = 1
1125 return result
1126
1127
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001128def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001129 """Returns information about the bots connected to the Swarming server."""
1130 add_filter_options(parser)
1131 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001132 '--dead-only', action='store_true',
1133 help='Only print dead bots, useful to reap them and reimage broken bots')
1134 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001135 '-k', '--keep-dead', action='store_true',
1136 help='Do not filter out dead bots')
1137 parser.filter_group.add_option(
1138 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001139 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001140 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001141
1142 if options.keep_dead and options.dead_only:
1143 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001144
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001145 bots = []
1146 cursor = None
1147 limit = 250
1148 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001149 base_url = (
maruel380e3262016-08-31 16:10:06 -07001150 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001151 while True:
1152 url = base_url
1153 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001154 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001155 data = net.url_read_json(url)
1156 if data is None:
1157 print >> sys.stderr, 'Failed to access %s' % options.swarming
1158 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001159 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001160 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001161 if not cursor:
1162 break
1163
maruel77f720b2015-09-15 12:35:22 -07001164 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001165 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001166 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001167 continue
maruel77f720b2015-09-15 12:35:22 -07001168 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001169 continue
1170
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001171 # If the user requested to filter on dimensions, ensure the bot has all the
1172 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001173 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001174 for key, value in options.dimensions:
1175 if key not in dimensions:
1176 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001177 # A bot can have multiple value for a key, for example,
1178 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1179 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001180 if isinstance(dimensions[key], list):
1181 if value not in dimensions[key]:
1182 break
1183 else:
1184 if value != dimensions[key]:
1185 break
1186 else:
maruel77f720b2015-09-15 12:35:22 -07001187 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001188 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001189 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001190 if bot.get('task_id'):
1191 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001192 return 0
1193
1194
maruelfd0a90c2016-06-10 11:51:10 -07001195@subcommand.usage('task_id')
1196def CMDcancel(parser, args):
1197 """Cancels a task."""
1198 options, args = parser.parse_args(args)
1199 if not args:
1200 parser.error('Please specify the task to cancel')
1201 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001202 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001203 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1204 print('Deleting %s failed. Probably already gone' % task_id)
1205 return 1
1206 return 0
1207
1208
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001209@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001210def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001211 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001212
1213 The result can be in multiple part if the execution was sharded. It can
1214 potentially have retries.
1215 """
1216 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001217 parser.add_option(
1218 '-j', '--json',
1219 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001220 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001221 if not args and not options.json:
1222 parser.error('Must specify at least one task id or --json.')
1223 if args and options.json:
1224 parser.error('Only use one of task id or --json.')
1225
1226 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001227 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001228 try:
maruel1ceb3872015-10-14 06:10:44 -07001229 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001230 data = json.load(f)
1231 except (IOError, ValueError):
1232 parser.error('Failed to open %s' % options.json)
1233 try:
1234 tasks = sorted(
1235 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1236 args = [t['task_id'] for t in tasks]
1237 except (KeyError, TypeError):
1238 parser.error('Failed to process %s' % options.json)
1239 if options.timeout is None:
1240 options.timeout = (
1241 data['request']['properties']['execution_timeout_secs'] +
1242 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001243 else:
1244 valid = frozenset('0123456789abcdef')
1245 if any(not valid.issuperset(task_id) for task_id in args):
1246 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001247
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001248 try:
1249 return collect(
1250 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001251 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001252 options.timeout,
1253 options.decorate,
1254 options.print_status_updates,
1255 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001256 options.task_output_dir,
1257 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001258 except Failure:
1259 on_error.report(None)
1260 return 1
1261
1262
maruelbea00862015-09-18 09:55:36 -07001263@subcommand.usage('[filename]')
1264def CMDput_bootstrap(parser, args):
1265 """Uploads a new version of bootstrap.py."""
1266 options, args = parser.parse_args(args)
1267 if len(args) != 1:
1268 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001269 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001270 path = unicode(os.path.abspath(args[0]))
1271 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001272 content = f.read().decode('utf-8')
1273 data = net.url_read_json(url, data={'content': content})
1274 print data
1275 return 0
1276
1277
1278@subcommand.usage('[filename]')
1279def CMDput_bot_config(parser, args):
1280 """Uploads a new version of bot_config.py."""
1281 options, args = parser.parse_args(args)
1282 if len(args) != 1:
1283 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001284 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001285 path = unicode(os.path.abspath(args[0]))
1286 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001287 content = f.read().decode('utf-8')
1288 data = net.url_read_json(url, data={'content': content})
1289 print data
1290 return 0
1291
1292
maruel77f720b2015-09-15 12:35:22 -07001293@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001294def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001295 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1296 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001297
1298 Examples:
maruel77f720b2015-09-15 12:35:22 -07001299 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001300 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001301
maruel77f720b2015-09-15 12:35:22 -07001302 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001303 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1304
1305 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1306 quoting is important!:
1307 swarming.py query -S server-url.com --limit 10 \\
1308 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001309 """
1310 CHUNK_SIZE = 250
1311
1312 parser.add_option(
1313 '-L', '--limit', type='int', default=200,
1314 help='Limit to enforce on limitless items (like number of tasks); '
1315 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001316 parser.add_option(
1317 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001318 parser.add_option(
1319 '--progress', action='store_true',
1320 help='Prints a dot at each request to show progress')
1321 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001322 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001323 parser.error(
1324 'Must specify only method name and optionally query args properly '
1325 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001326 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001327 url = base_url
1328 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001329 # Check check, change if not working out.
1330 merge_char = '&' if '?' in url else '?'
1331 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001332 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001333 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001334 # TODO(maruel): Do basic diagnostic.
1335 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001336 return 1
1337
1338 # Some items support cursors. Try to get automatically if cursors are needed
1339 # by looking at the 'cursor' items.
1340 while (
1341 data.get('cursor') and
1342 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001343 merge_char = '&' if '?' in base_url else '?'
1344 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001345 if options.limit:
1346 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001347 if options.progress:
1348 sys.stdout.write('.')
1349 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001350 new = net.url_read_json(url)
1351 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001352 if options.progress:
1353 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001354 print >> sys.stderr, 'Failed to access %s' % options.swarming
1355 return 1
maruel81b37132015-10-21 06:42:13 -07001356 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001357 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001358
maruel77f720b2015-09-15 12:35:22 -07001359 if options.progress:
1360 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001361 if options.limit and len(data.get('items', [])) > options.limit:
1362 data['items'] = data['items'][:options.limit]
1363 data.pop('cursor', None)
1364
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001365 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001366 options.json = unicode(os.path.abspath(options.json))
1367 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001368 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001369 try:
maruel77f720b2015-09-15 12:35:22 -07001370 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001371 sys.stdout.write('\n')
1372 except IOError:
1373 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001374 return 0
1375
1376
maruel77f720b2015-09-15 12:35:22 -07001377def CMDquery_list(parser, args):
1378 """Returns list of all the Swarming APIs that can be used with command
1379 'query'.
1380 """
1381 parser.add_option(
1382 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1383 options, args = parser.parse_args(args)
1384 if args:
1385 parser.error('No argument allowed.')
1386
1387 try:
1388 apis = endpoints_api_discovery_apis(options.swarming)
1389 except APIError as e:
1390 parser.error(str(e))
1391 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001392 options.json = unicode(os.path.abspath(options.json))
1393 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001394 json.dump(apis, f)
1395 else:
1396 help_url = (
1397 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1398 options.swarming)
1399 for api_id, api in sorted(apis.iteritems()):
1400 print api_id
1401 print ' ' + api['description']
1402 for resource_name, resource in sorted(api['resources'].iteritems()):
1403 print ''
1404 for method_name, method in sorted(resource['methods'].iteritems()):
1405 # Only list the GET ones.
1406 if method['httpMethod'] != 'GET':
1407 continue
1408 print '- %s.%s: %s' % (
1409 resource_name, method_name, method['path'])
1410 print ' ' + method['description']
1411 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1412 return 0
1413
1414
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001415@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001416def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001417 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001418
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001419 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001420 """
1421 add_trigger_options(parser)
1422 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001423 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001424 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001425 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001426 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001427 tasks = trigger_task_shards(
1428 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001429 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001430 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001431 'Failed to trigger %s(%s): %s' %
1432 (options.task_name, args[0], e.args[0]))
1433 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001434 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001435 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001436 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001437 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001438 task_ids = [
1439 t['task_id']
1440 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1441 ]
maruel71c61c82016-02-22 06:52:05 -08001442 if options.timeout is None:
1443 options.timeout = (
1444 task_request.properties.execution_timeout_secs +
1445 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001446 try:
1447 return collect(
1448 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001449 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001450 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001451 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001452 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001453 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001454 options.task_output_dir,
1455 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001456 except Failure:
1457 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001458 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001459
1460
maruel18122c62015-10-23 06:31:23 -07001461@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001462def CMDreproduce(parser, args):
1463 """Runs a task locally that was triggered on the server.
1464
1465 This running locally the same commands that have been run on the bot. The data
1466 downloaded will be in a subdirectory named 'work' of the current working
1467 directory.
maruel18122c62015-10-23 06:31:23 -07001468
1469 You can pass further additional arguments to the target command by passing
1470 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001471 """
maruelc070e672016-02-22 17:32:57 -08001472 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001473 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001474 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001475 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001476 extra_args = []
1477 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001478 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001479 if len(args) > 1:
1480 if args[1] == '--':
1481 if len(args) > 2:
1482 extra_args = args[2:]
1483 else:
1484 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001485
maruel380e3262016-08-31 16:10:06 -07001486 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001487 request = net.url_read_json(url)
1488 if not request:
1489 print >> sys.stderr, 'Failed to retrieve request data for the task'
1490 return 1
1491
maruel12e30012015-10-09 11:55:35 -07001492 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001493 if fs.isdir(workdir):
1494 parser.error('Please delete the directory \'work\' first')
1495 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001496
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001497 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001498 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001499 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001500 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001501 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001502 for i in properties['env']:
1503 key = i['key'].encode('utf-8')
1504 if not i['value']:
1505 env.pop(key, None)
1506 else:
1507 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001508
nodir152cba62016-05-12 16:08:56 -07001509 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001510 # Create the tree.
1511 with isolateserver.get_storage(
1512 properties['inputs_ref']['isolatedserver'],
1513 properties['inputs_ref']['namespace']) as storage:
1514 bundle = isolateserver.fetch_isolated(
1515 properties['inputs_ref']['isolated'],
1516 storage,
1517 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001518 workdir,
1519 False)
maruel29ab2fd2015-10-16 11:44:01 -07001520 command = bundle.command
1521 if bundle.relative_cwd:
1522 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001523 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001524 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001525 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001526 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001527 if not options.output_dir and new_command != command:
1528 parser.error('The task has outputs, you must use --output-dir')
1529 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001530 else:
1531 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001532 try:
maruel18122c62015-10-23 06:31:23 -07001533 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001534 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001535 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001536 print >> sys.stderr, str(e)
1537 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001538
1539
maruel0eb1d1b2015-10-02 14:48:21 -07001540@subcommand.usage('bot_id')
1541def CMDterminate(parser, args):
1542 """Tells a bot to gracefully shut itself down as soon as it can.
1543
1544 This is done by completing whatever current task there is then exiting the bot
1545 process.
1546 """
1547 parser.add_option(
1548 '--wait', action='store_true', help='Wait for the bot to terminate')
1549 options, args = parser.parse_args(args)
1550 if len(args) != 1:
1551 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001552 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001553 request = net.url_read_json(url, data={})
1554 if not request:
1555 print >> sys.stderr, 'Failed to ask for termination'
1556 return 1
1557 if options.wait:
1558 return collect(
maruel9531ce02016-04-13 06:11:23 -07001559 options.swarming, [request['task_id']], 0., False, False, None, None,
1560 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001561 return 0
1562
1563
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001564@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001565def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001566 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001567
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001568 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001569 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001570
1571 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001572
1573 Passes all extra arguments provided after '--' as additional command line
1574 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001575 """
1576 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001577 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001578 parser.add_option(
1579 '--dump-json',
1580 metavar='FILE',
1581 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001582 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001583 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001584 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001585 tasks = trigger_task_shards(
1586 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001587 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001588 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001589 tasks_sorted = sorted(
1590 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001591 if options.dump_json:
1592 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001593 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001594 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001595 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001596 }
maruel46b015f2015-10-13 18:40:35 -07001597 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001598 print('To collect results, use:')
1599 print(' swarming.py collect -S %s --json %s' %
1600 (options.swarming, options.dump_json))
1601 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001602 print('To collect results, use:')
1603 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001604 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1605 print('Or visit:')
1606 for t in tasks_sorted:
1607 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001608 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001609 except Failure:
1610 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001611 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001612
1613
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001614class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001615 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001616 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001617 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001618 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001619 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001620 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001621 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001622 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001623 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001624 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001625
1626 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001627 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001628 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001629 auth.process_auth_options(self, options)
1630 user = self._process_swarming(options)
1631 if hasattr(options, 'user') and not options.user:
1632 options.user = user
1633 return options, args
1634
1635 def _process_swarming(self, options):
1636 """Processes the --swarming option and aborts if not specified.
1637
1638 Returns the identity as determined by the server.
1639 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001640 if not options.swarming:
1641 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001642 try:
1643 options.swarming = net.fix_url(options.swarming)
1644 except ValueError as e:
1645 self.error('--swarming %s' % e)
1646 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001647 try:
1648 user = auth.ensure_logged_in(options.swarming)
1649 except ValueError as e:
1650 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001651 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001652
1653
1654def main(args):
1655 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001656 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001657
1658
1659if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001660 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001661 fix_encoding.fix_encoding()
1662 tools.disable_buffering()
1663 colorama.init()
1664 sys.exit(main(sys.argv[1:]))