blob: 90552540bdeec72e384b61b641e4fff22a66eccd [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
maruel681d6802017-01-17 16:56:03 -08008__version__ = '0.8.9'
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 [
maruel681d6802017-01-17 16:56:03 -0800168 'caches',
borenet02f772b2016-06-22 12:42:19 -0700169 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 'dimensions',
172 'env',
maruel77f720b2015-09-15 12:35:22 -0700173 'execution_timeout_secs',
174 'extra_args',
175 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500176 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700177 'inputs_ref',
178 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700179 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700180 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700181 ])
182
183
184# See ../appengine/swarming/swarming_rpcs.py.
185NewTaskRequest = collections.namedtuple(
186 'NewTaskRequest',
187 [
188 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500189 'name',
maruel77f720b2015-09-15 12:35:22 -0700190 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500191 'priority',
maruel77f720b2015-09-15 12:35:22 -0700192 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700193 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500194 'tags',
195 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500196 ])
197
198
maruel77f720b2015-09-15 12:35:22 -0700199def namedtuple_to_dict(value):
200 """Recursively converts a namedtuple to a dict."""
201 out = dict(value._asdict())
202 for k, v in out.iteritems():
203 if hasattr(v, '_asdict'):
204 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700205 elif isinstance(v, (list, tuple)):
206 l = []
207 for elem in v:
208 if hasattr(elem, '_asdict'):
209 l.append(namedtuple_to_dict(elem))
210 else:
211 l.append(elem)
212 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700213 return out
214
215
vadimsh93d167c2016-09-13 11:31:51 -0700216def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800217 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700218
219 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500220 """
maruel77f720b2015-09-15 12:35:22 -0700221 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700222 if hide_token:
223 if out['service_account_token'] not in (None, 'bot', 'none'):
224 out['service_account_token'] = '<hidden>'
225 # Don't send 'service_account_token' if it is None to avoid confusing older
226 # version of the server that doesn't know about 'service_account_token'.
227 if out['service_account_token'] in (None, 'none'):
228 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700229 # Maps are not supported until protobuf v3.
230 out['properties']['dimensions'] = [
231 {'key': k, 'value': v}
232 for k, v in out['properties']['dimensions'].iteritems()
233 ]
234 out['properties']['dimensions'].sort(key=lambda x: x['key'])
235 out['properties']['env'] = [
236 {'key': k, 'value': v}
237 for k, v in out['properties']['env'].iteritems()
238 ]
239 out['properties']['env'].sort(key=lambda x: x['key'])
240 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500241
242
maruel77f720b2015-09-15 12:35:22 -0700243def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500244 """Triggers a request on the Swarming server and returns the json data.
245
246 It's the low-level function.
247
248 Returns:
249 {
250 'request': {
251 'created_ts': u'2010-01-02 03:04:05',
252 'name': ..
253 },
254 'task_id': '12300',
255 }
256 """
257 logging.info('Triggering: %s', raw_request['name'])
258
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500259 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700260 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500261 if not result:
262 on_error.report('Failed to trigger task %s' % raw_request['name'])
263 return None
maruele557bce2015-11-17 09:01:27 -0800264 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800265 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800266 msg = 'Failed to trigger task %s' % raw_request['name']
267 if result['error'].get('errors'):
268 for err in result['error']['errors']:
269 if err.get('message'):
270 msg += '\nMessage: %s' % err['message']
271 if err.get('debugInfo'):
272 msg += '\nDebug info:\n%s' % err['debugInfo']
273 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800274 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800275
276 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800277 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500278 return result
279
280
281def setup_googletest(env, shards, index):
282 """Sets googletest specific environment variables."""
283 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700284 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
285 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
286 env = env[:]
287 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
288 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500289 return env
290
291
292def trigger_task_shards(swarming, task_request, shards):
293 """Triggers one or many subtasks of a sharded task.
294
295 Returns:
296 Dict with task details, returned to caller as part of --dump-json output.
297 None in case of failure.
298 """
299 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700300 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500301 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700302 req['properties']['env'] = setup_googletest(
303 req['properties']['env'], shards, index)
304 req['name'] += ':%s:%s' % (index, shards)
305 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500306
307 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500308 tasks = {}
309 priority_warning = False
310 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700311 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500312 if not task:
313 break
314 logging.info('Request result: %s', task)
315 if (not priority_warning and
316 task['request']['priority'] != task_request.priority):
317 priority_warning = True
318 print >> sys.stderr, (
319 'Priority was reset to %s' % task['request']['priority'])
320 tasks[request['name']] = {
321 'shard_index': index,
322 'task_id': task['task_id'],
323 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
324 }
325
326 # Some shards weren't triggered. Abort everything.
327 if len(tasks) != len(requests):
328 if tasks:
329 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
330 len(tasks), len(requests))
331 for task_dict in tasks.itervalues():
332 abort_task(swarming, task_dict['task_id'])
333 return None
334
335 return tasks
336
337
vadimsh93d167c2016-09-13 11:31:51 -0700338def mint_service_account_token(service_account):
339 """Given a service account name returns a delegation token for this account.
340
341 The token is generated based on triggering user's credentials. It is passed
342 to Swarming, that uses it when running tasks.
343 """
344 logging.info(
345 'Generating delegation token for service account "%s"', service_account)
346 raise NotImplementedError('Custom service accounts are not implemented yet')
347
348
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500349### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000350
351
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700352# How often to print status updates to stdout in 'collect'.
353STATUS_UPDATE_INTERVAL = 15 * 60.
354
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400355
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400356class State(object):
357 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000358
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400359 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
360 values are part of the API so if they change, the API changed.
361
362 It's in fact an enum. Values should be in decreasing order of importance.
363 """
364 RUNNING = 0x10
365 PENDING = 0x20
366 EXPIRED = 0x30
367 TIMED_OUT = 0x40
368 BOT_DIED = 0x50
369 CANCELED = 0x60
370 COMPLETED = 0x70
371
maruel77f720b2015-09-15 12:35:22 -0700372 STATES = (
373 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
374 'COMPLETED')
375 STATES_RUNNING = ('RUNNING', 'PENDING')
376 STATES_NOT_RUNNING = (
377 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
378 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
379 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400380
381 _NAMES = {
382 RUNNING: 'Running',
383 PENDING: 'Pending',
384 EXPIRED: 'Expired',
385 TIMED_OUT: 'Execution timed out',
386 BOT_DIED: 'Bot died',
387 CANCELED: 'User canceled',
388 COMPLETED: 'Completed',
389 }
390
maruel77f720b2015-09-15 12:35:22 -0700391 _ENUMS = {
392 'RUNNING': RUNNING,
393 'PENDING': PENDING,
394 'EXPIRED': EXPIRED,
395 'TIMED_OUT': TIMED_OUT,
396 'BOT_DIED': BOT_DIED,
397 'CANCELED': CANCELED,
398 'COMPLETED': COMPLETED,
399 }
400
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400401 @classmethod
402 def to_string(cls, state):
403 """Returns a user-readable string representing a State."""
404 if state not in cls._NAMES:
405 raise ValueError('Invalid state %s' % state)
406 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000407
maruel77f720b2015-09-15 12:35:22 -0700408 @classmethod
409 def from_enum(cls, state):
410 """Returns int value based on the string."""
411 if state not in cls._ENUMS:
412 raise ValueError('Invalid state %s' % state)
413 return cls._ENUMS[state]
414
maruel@chromium.org0437a732013-08-27 16:05:52 +0000415
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700416class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700417 """Assembles task execution summary (for --task-summary-json output).
418
419 Optionally fetches task outputs from isolate server to local disk (used when
420 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700421
422 This object is shared among multiple threads running 'retrieve_results'
423 function, in particular they call 'process_shard_result' method in parallel.
424 """
425
maruel0eb1d1b2015-10-02 14:48:21 -0700426 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
428
429 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700430 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700431 shard_count: expected number of task shards.
432 """
maruel12e30012015-10-09 11:55:35 -0700433 self.task_output_dir = (
434 unicode(os.path.abspath(task_output_dir))
435 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700436 self.shard_count = shard_count
437
438 self._lock = threading.Lock()
439 self._per_shard_results = {}
440 self._storage = None
441
nodire5028a92016-04-29 14:38:21 -0700442 if self.task_output_dir:
443 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444
Vadim Shtayurab450c602014-05-12 19:23:25 -0700445 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700446 """Stores results of a single task shard, fetches output files if necessary.
447
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400448 Modifies |result| in place.
449
maruel77f720b2015-09-15 12:35:22 -0700450 shard_index is 0-based.
451
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452 Called concurrently from multiple threads.
453 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700455 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 if shard_index < 0 or shard_index >= self.shard_count:
457 logging.warning(
458 'Shard index %d is outside of expected range: [0; %d]',
459 shard_index, self.shard_count - 1)
460 return
461
maruel77f720b2015-09-15 12:35:22 -0700462 if result.get('outputs_ref'):
463 ref = result['outputs_ref']
464 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
465 ref['isolatedserver'],
466 urllib.urlencode(
467 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400468
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700469 # Store result dict of that shard, ignore results we've already seen.
470 with self._lock:
471 if shard_index in self._per_shard_results:
472 logging.warning('Ignoring duplicate shard index %d', shard_index)
473 return
474 self._per_shard_results[shard_index] = result
475
476 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700477 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400478 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700479 result['outputs_ref']['isolatedserver'],
480 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400481 if storage:
482 # Output files are supposed to be small and they are not reused across
483 # tasks. So use MemoryCache for them instead of on-disk cache. Make
484 # files writable, so that calling script can delete them.
485 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700486 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400487 storage,
488 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700489 os.path.join(self.task_output_dir, str(shard_index)),
490 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700491
492 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700493 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700494 with self._lock:
495 # Write an array of shard results with None for missing shards.
496 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700497 'shards': [
498 self._per_shard_results.get(i) for i in xrange(self.shard_count)
499 ],
500 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700501 # Write summary.json to task_output_dir as well.
502 if self.task_output_dir:
503 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700504 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700505 summary,
506 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700507 if self._storage:
508 self._storage.close()
509 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700510 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700511
512 def _get_storage(self, isolate_server, namespace):
513 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700514 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700515 with self._lock:
516 if not self._storage:
517 self._storage = isolateserver.get_storage(isolate_server, namespace)
518 else:
519 # Shards must all use exact same isolate server and namespace.
520 if self._storage.location != isolate_server:
521 logging.error(
522 'Task shards are using multiple isolate servers: %s and %s',
523 self._storage.location, isolate_server)
524 return None
525 if self._storage.namespace != namespace:
526 logging.error(
527 'Task shards are using multiple namespaces: %s and %s',
528 self._storage.namespace, namespace)
529 return None
530 return self._storage
531
532
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500533def now():
534 """Exists so it can be mocked easily."""
535 return time.time()
536
537
maruel77f720b2015-09-15 12:35:22 -0700538def parse_time(value):
539 """Converts serialized time from the API to datetime.datetime."""
540 # When microseconds are 0, the '.123456' suffix is elided. This means the
541 # serialized format is not consistent, which confuses the hell out of python.
542 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
543 try:
544 return datetime.datetime.strptime(value, fmt)
545 except ValueError:
546 pass
547 raise ValueError('Failed to parse %s' % value)
548
549
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700550def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700551 base_url, shard_index, task_id, timeout, should_stop, output_collector,
552 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400553 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700554
Vadim Shtayurab450c602014-05-12 19:23:25 -0700555 Returns:
556 <result dict> on success.
557 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700558 """
maruel71c61c82016-02-22 06:52:05 -0800559 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700560 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700561 if include_perf:
562 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700563 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700564 started = now()
565 deadline = started + timeout if timeout else None
566 attempt = 0
567
568 while not should_stop.is_set():
569 attempt += 1
570
571 # Waiting for too long -> give up.
572 current_time = now()
573 if deadline and current_time >= deadline:
574 logging.error('retrieve_results(%s) timed out on attempt %d',
575 base_url, attempt)
576 return None
577
578 # Do not spin too fast. Spin faster at the beginning though.
579 # Start with 1 sec delay and for each 30 sec of waiting add another second
580 # of delay, until hitting 15 sec ceiling.
581 if attempt > 1:
582 max_delay = min(15, 1 + (current_time - started) / 30.0)
583 delay = min(max_delay, deadline - current_time) if deadline else max_delay
584 if delay > 0:
585 logging.debug('Waiting %.1f sec before retrying', delay)
586 should_stop.wait(delay)
587 if should_stop.is_set():
588 return None
589
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400590 # Disable internal retries in net.url_read_json, since we are doing retries
591 # ourselves.
592 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700593 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
594 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400595 result = net.url_read_json(result_url, retry_50x=False)
596 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400597 continue
maruel77f720b2015-09-15 12:35:22 -0700598
maruelbf53e042015-12-01 15:00:51 -0800599 if result.get('error'):
600 # An error occurred.
601 if result['error'].get('errors'):
602 for err in result['error']['errors']:
603 logging.warning(
604 'Error while reading task: %s; %s',
605 err.get('message'), err.get('debugInfo'))
606 elif result['error'].get('message'):
607 logging.warning(
608 'Error while reading task: %s', result['error']['message'])
609 continue
610
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400611 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700612 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400613 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700614 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700615 # Record the result, try to fetch attached output files (if any).
616 if output_collector:
617 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700618 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700619 if result.get('internal_failure'):
620 logging.error('Internal error!')
621 elif result['state'] == 'BOT_DIED':
622 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700623 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624
625
maruel77f720b2015-09-15 12:35:22 -0700626def convert_to_old_format(result):
627 """Converts the task result data from Endpoints API format to old API format
628 for compatibility.
629
630 This goes into the file generated as --task-summary-json.
631 """
632 # Sets default.
633 result.setdefault('abandoned_ts', None)
634 result.setdefault('bot_id', None)
635 result.setdefault('bot_version', None)
636 result.setdefault('children_task_ids', [])
637 result.setdefault('completed_ts', None)
638 result.setdefault('cost_saved_usd', None)
639 result.setdefault('costs_usd', None)
640 result.setdefault('deduped_from', None)
641 result.setdefault('name', None)
642 result.setdefault('outputs_ref', None)
643 result.setdefault('properties_hash', None)
644 result.setdefault('server_versions', None)
645 result.setdefault('started_ts', None)
646 result.setdefault('tags', None)
647 result.setdefault('user', None)
648
649 # Convertion back to old API.
650 duration = result.pop('duration', None)
651 result['durations'] = [duration] if duration else []
652 exit_code = result.pop('exit_code', None)
653 result['exit_codes'] = [int(exit_code)] if exit_code else []
654 result['id'] = result.pop('task_id')
655 result['isolated_out'] = result.get('outputs_ref', None)
656 output = result.pop('output', None)
657 result['outputs'] = [output] if output else []
658 # properties_hash
659 # server_version
660 # Endpoints result 'state' as string. For compatibility with old code, convert
661 # to int.
662 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700663 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700664 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700665 if 'bot_dimensions' in result:
666 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700667 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700668 }
669 else:
670 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700671
672
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700673def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400674 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700675 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500676 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 Duplicate shards are ignored. Shards are yielded in order of completion.
679 Timed out shards are NOT yielded at all. Caller can compare number of yielded
680 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681
682 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500683 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 +0000684 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500685
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700686 output_collector is an optional instance of TaskOutputCollector that will be
687 used to fetch files produced by a task from isolate server to the local disk.
688
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500689 Yields:
690 (index, result). In particular, 'result' is defined as the
691 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000692 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000693 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400694 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700695 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700696 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700697
maruel@chromium.org0437a732013-08-27 16:05:52 +0000698 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
699 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700700 # Adds a task to the thread pool to call 'retrieve_results' and return
701 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400702 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700703 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000704 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400705 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700706 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700707
708 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400709 for shard_index, task_id in enumerate(task_ids):
710 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700711
712 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400713 shards_remaining = range(len(task_ids))
714 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700715 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700716 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700717 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700718 shard_index, result = results_channel.pull(
719 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700720 except threading_utils.TaskChannel.Timeout:
721 if print_status_updates:
722 print(
723 'Waiting for results from the following shards: %s' %
724 ', '.join(map(str, shards_remaining)))
725 sys.stdout.flush()
726 continue
727 except Exception:
728 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700729
730 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700731 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000732 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500733 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000734 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700735
Vadim Shtayurab450c602014-05-12 19:23:25 -0700736 # Yield back results to the caller.
737 assert shard_index in shards_remaining
738 shards_remaining.remove(shard_index)
739 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700740
maruel@chromium.org0437a732013-08-27 16:05:52 +0000741 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700742 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000743 should_stop.set()
744
745
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400746def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000747 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700748 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400749 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700750 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
751 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400752 else:
753 pending = 'N/A'
754
maruel77f720b2015-09-15 12:35:22 -0700755 if metadata.get('duration') is not None:
756 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400757 else:
758 duration = 'N/A'
759
maruel77f720b2015-09-15 12:35:22 -0700760 if metadata.get('exit_code') is not None:
761 # Integers are encoded as string to not loose precision.
762 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400763 else:
764 exit_code = 'N/A'
765
766 bot_id = metadata.get('bot_id') or 'N/A'
767
maruel77f720b2015-09-15 12:35:22 -0700768 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400769 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400770 tag_footer = (
771 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
772 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400773
774 tag_len = max(len(tag_header), len(tag_footer))
775 dash_pad = '+-%s-+\n' % ('-' * tag_len)
776 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
777 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
778
779 header = dash_pad + tag_header + dash_pad
780 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700781 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400782 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000783
784
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700785def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700786 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700787 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700788 """Retrieves results of a Swarming task.
789
790 Returns:
791 process exit code that should be returned to the user.
792 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700793 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700794 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700795
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700796 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700797 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400798 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700799 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400800 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400801 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700802 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700803 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700804
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400805 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700806 shard_exit_code = metadata.get('exit_code')
807 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700808 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700809 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700810 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400811 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700812 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700813
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700814 if decorate:
leileied181762016-10-13 14:24:59 -0700815 s = decorate_shard_output(swarming, index, metadata).encode(
816 'utf-8', 'replace')
817 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400818 if len(seen_shards) < len(task_ids):
819 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700820 else:
maruel77f720b2015-09-15 12:35:22 -0700821 print('%s: %s %s' % (
822 metadata.get('bot_id', 'N/A'),
823 metadata['task_id'],
824 shard_exit_code))
825 if metadata['output']:
826 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400827 if output:
828 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700829 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700830 summary = output_collector.finalize()
831 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700832 # TODO(maruel): Make this optional.
833 for i in summary['shards']:
834 if i:
835 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700836 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700837
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400838 if decorate and total_duration:
839 print('Total duration: %.1fs' % total_duration)
840
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400841 if len(seen_shards) != len(task_ids):
842 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700843 print >> sys.stderr, ('Results from some shards are missing: %s' %
844 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700845 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700846
maruela5490782015-09-30 10:56:59 -0700847 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000848
849
maruel77f720b2015-09-15 12:35:22 -0700850### API management.
851
852
853class APIError(Exception):
854 pass
855
856
857def endpoints_api_discovery_apis(host):
858 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
859 the APIs exposed by a host.
860
861 https://developers.google.com/discovery/v1/reference/apis/list
862 """
maruel380e3262016-08-31 16:10:06 -0700863 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
864 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700865 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
866 if data is None:
867 raise APIError('Failed to discover APIs on %s' % host)
868 out = {}
869 for api in data['items']:
870 if api['id'] == 'discovery:v1':
871 continue
872 # URL is of the following form:
873 # url = host + (
874 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
875 api_data = net.url_read_json(api['discoveryRestUrl'])
876 if api_data is None:
877 raise APIError('Failed to discover %s on %s' % (api['id'], host))
878 out[api['id']] = api_data
879 return out
880
881
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500882### Commands.
883
884
885def abort_task(_swarming, _manifest):
886 """Given a task manifest that was triggered, aborts its execution."""
887 # TODO(vadimsh): No supported by the server yet.
888
889
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400890def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800891 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500892 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500893 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500894 dest='dimensions', metavar='FOO bar',
895 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500896 parser.add_option_group(parser.filter_group)
897
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400898
Vadim Shtayurab450c602014-05-12 19:23:25 -0700899def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400900 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700901 parser.sharding_group.add_option(
902 '--shards', type='int', default=1,
903 help='Number of shards to trigger and collect.')
904 parser.add_option_group(parser.sharding_group)
905
906
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400907def add_trigger_options(parser):
908 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500909 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400910 add_filter_options(parser)
911
maruel681d6802017-01-17 16:56:03 -0800912 group = optparse.OptionGroup(parser, 'Task properties')
913 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500914 '-s', '--isolated',
915 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800916 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500917 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700918 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800919 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400920 '--idempotent', action='store_true', default=False,
921 help='When set, the server will actively try to find a previous task '
922 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800923 group.add_option(
iannuccieee1bca2016-10-28 13:16:23 -0700924 '--secret-bytes-path',
iannuccidc80dfb2016-10-28 12:50:20 -0700925 help='The optional path to a file containing the secret_bytes to use with'
926 'this task.')
maruel681d6802017-01-17 16:56:03 -0800927 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400928 '--hard-timeout', type='int', default=60*60,
929 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800930 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400931 '--io-timeout', type='int', default=20*60,
932 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800933 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500934 '--raw-cmd', action='store_true', default=False,
935 help='When set, the command after -- is used as-is without run_isolated. '
936 'In this case, no .isolated file is expected.')
maruel681d6802017-01-17 16:56:03 -0800937 group.add_option(
borenet02f772b2016-06-22 12:42:19 -0700938 '--cipd-package', action='append', default=[],
939 help='CIPD packages to install on the Swarming bot. Uses the format: '
940 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800941 group.add_option(
942 '--named-cache', action='append', nargs=2, default=[],
943 help='"<name> <relpath>" items to keep a persistent bot managed cache')
944 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700945 '--service-account',
946 help='Name of a service account to run the task as. Only literal "bot" '
947 'string can be specified currently (to run the task under bot\'s '
948 'account). Don\'t use task service accounts if not given '
949 '(default).')
maruel681d6802017-01-17 16:56:03 -0800950 group.add_option(
aludwincc5524e2016-10-28 10:25:24 -0700951 '-o', '--output', action='append', default=[],
952 help='A list of files to return in addition to those written to'
953 '$(ISOLATED_OUTDIR). An error will occur if a file specified by'
954 'this option is also written directly to $(ISOLATED_OUTDIR).')
maruel681d6802017-01-17 16:56:03 -0800955 parser.add_option_group(group)
956
957 group = optparse.OptionGroup(parser, 'Task request')
958 group.add_option(
959 '--priority', type='int', default=100,
960 help='The lower value, the more important the task is')
961 group.add_option(
962 '-T', '--task-name',
963 help='Display name of the task. Defaults to '
964 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
965 'isolated file is provided, if a hash is provided, it defaults to '
966 '<user>/<dimensions>/<isolated hash>/<timestamp>')
967 group.add_option(
968 '--tags', action='append', default=[],
969 help='Tags to assign to the task.')
970 group.add_option(
971 '--user', default='',
972 help='User associated with the task. Defaults to authenticated user on '
973 'the server.')
974 group.add_option(
975 '--expiration', type='int', default=6*60*60,
976 help='Seconds to allow the task to be pending for a bot to run before '
977 'this task request expires.')
978 group.add_option(
979 '--deadline', type='int', dest='expiration',
980 help=optparse.SUPPRESS_HELP)
981 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000982
983
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500984def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700985 """Processes trigger options and does preparatory steps.
986
987 Uploads files to isolate server and generates service account tokens if
988 necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500989 """
990 options.dimensions = dict(options.dimensions)
991 options.env = dict(options.env)
992
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500993 if not options.dimensions:
994 parser.error('Please at least specify one --dimension')
995 if options.raw_cmd:
996 if not args:
997 parser.error(
998 'Arguments with --raw-cmd should be passed after -- as command '
999 'delimiter.')
1000 if options.isolate_server:
1001 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
1002
1003 command = args
1004 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001005 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001006 options.user,
1007 '_'.join(
1008 '%s=%s' % (k, v)
1009 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -07001010 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001011 else:
nodir55be77b2016-05-03 09:39:57 -07001012 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001013 try:
maruel77f720b2015-09-15 12:35:22 -07001014 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001015 except ValueError as e:
1016 parser.error(str(e))
1017
borenet02f772b2016-06-22 12:42:19 -07001018 cipd_packages = []
1019 for p in options.cipd_package:
1020 split = p.split(':', 2)
1021 if len(split) != 3:
1022 parser.error('CIPD packages must take the form: path:package:version')
1023 cipd_packages.append(CipdPackage(
1024 package_name=split[1],
1025 path=split[0],
1026 version=split[2]))
1027 cipd_input = None
1028 if cipd_packages:
1029 cipd_input = CipdInput(
1030 client_package=None,
1031 packages=cipd_packages,
1032 server=None)
1033
iannuccidc80dfb2016-10-28 12:50:20 -07001034 secret_bytes = None
1035 if options.secret_bytes_path:
1036 with open(options.secret_bytes_path, 'r') as f:
1037 secret_bytes = f.read().encode('base64')
1038
maruel681d6802017-01-17 16:56:03 -08001039 caches = [
1040 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1041 for i in options.named_cache
1042 ]
nodir152cba62016-05-12 16:08:56 -07001043 # If inputs_ref.isolated is used, command is actually extra_args.
1044 # Otherwise it's an actual command to run.
1045 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -07001046 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001047 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001048 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -07001049 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001050 dimensions=options.dimensions,
1051 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001052 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -07001053 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -07001054 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001055 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001056 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001057 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001058 outputs=options.output,
1059 secret_bytes=secret_bytes)
maruel8fce7962015-10-21 11:17:47 -07001060 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1061 parser.error('--tags must be in the format key:value')
vadimsh93d167c2016-09-13 11:31:51 -07001062
1063 # Convert a service account email to a signed service account token to pass
1064 # to Swarming.
1065 service_account_token = None
1066 if options.service_account in ('bot', 'none'):
1067 service_account_token = options.service_account
1068 elif options.service_account:
1069 # pylint: disable=assignment-from-no-return
1070 service_account_token = mint_service_account_token(options.service_account)
1071
maruel77f720b2015-09-15 12:35:22 -07001072 return NewTaskRequest(
1073 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001074 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001075 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001077 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001078 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001079 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001080 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001081
1082
1083def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001084 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001085 '-t', '--timeout', type='float',
1086 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1087 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001088 parser.group_logging.add_option(
1089 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001090 parser.group_logging.add_option(
1091 '--print-status-updates', action='store_true',
1092 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001093 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001094 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001095 '--task-summary-json',
1096 metavar='FILE',
1097 help='Dump a summary of task results to this file as json. It contains '
1098 'only shards statuses as know to server directly. Any output files '
1099 'emitted by the task can be collected by using --task-output-dir')
1100 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001101 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001102 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001103 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001104 'directory contains per-shard directory with output files produced '
1105 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001106 parser.task_output_group.add_option(
1107 '--perf', action='store_true', default=False,
1108 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001109 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001110
1111
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001112@subcommand.usage('bots...')
1113def CMDbot_delete(parser, args):
1114 """Forcibly deletes bots from the Swarming server."""
1115 parser.add_option(
1116 '-f', '--force', action='store_true',
1117 help='Do not prompt for confirmation')
1118 options, args = parser.parse_args(args)
1119 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001120 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001121
1122 bots = sorted(args)
1123 if not options.force:
1124 print('Delete the following bots?')
1125 for bot in bots:
1126 print(' %s' % bot)
1127 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1128 print('Goodbye.')
1129 return 1
1130
1131 result = 0
1132 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001133 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001134 if net.url_read_json(url, data={}, method='POST') is None:
1135 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001136 result = 1
1137 return result
1138
1139
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001140def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001141 """Returns information about the bots connected to the Swarming server."""
1142 add_filter_options(parser)
1143 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001144 '--dead-only', action='store_true',
1145 help='Only print dead bots, useful to reap them and reimage broken bots')
1146 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001147 '-k', '--keep-dead', action='store_true',
1148 help='Do not filter out dead bots')
1149 parser.filter_group.add_option(
1150 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001151 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001152 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001153
1154 if options.keep_dead and options.dead_only:
1155 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001156
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001157 bots = []
1158 cursor = None
1159 limit = 250
1160 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001161 base_url = (
maruel380e3262016-08-31 16:10:06 -07001162 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001163 while True:
1164 url = base_url
1165 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001166 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001167 data = net.url_read_json(url)
1168 if data is None:
1169 print >> sys.stderr, 'Failed to access %s' % options.swarming
1170 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001171 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001172 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001173 if not cursor:
1174 break
1175
maruel77f720b2015-09-15 12:35:22 -07001176 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001177 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001178 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001179 continue
maruel77f720b2015-09-15 12:35:22 -07001180 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001181 continue
1182
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001183 # If the user requested to filter on dimensions, ensure the bot has all the
1184 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001185 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001186 for key, value in options.dimensions:
1187 if key not in dimensions:
1188 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001189 # A bot can have multiple value for a key, for example,
1190 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1191 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001192 if isinstance(dimensions[key], list):
1193 if value not in dimensions[key]:
1194 break
1195 else:
1196 if value != dimensions[key]:
1197 break
1198 else:
maruel77f720b2015-09-15 12:35:22 -07001199 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001200 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001201 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001202 if bot.get('task_id'):
1203 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001204 return 0
1205
1206
maruelfd0a90c2016-06-10 11:51:10 -07001207@subcommand.usage('task_id')
1208def CMDcancel(parser, args):
1209 """Cancels a task."""
1210 options, args = parser.parse_args(args)
1211 if not args:
1212 parser.error('Please specify the task to cancel')
1213 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001214 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001215 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1216 print('Deleting %s failed. Probably already gone' % task_id)
1217 return 1
1218 return 0
1219
1220
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001221@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001222def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001223 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001224
1225 The result can be in multiple part if the execution was sharded. It can
1226 potentially have retries.
1227 """
1228 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001229 parser.add_option(
1230 '-j', '--json',
1231 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001232 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001233 if not args and not options.json:
1234 parser.error('Must specify at least one task id or --json.')
1235 if args and options.json:
1236 parser.error('Only use one of task id or --json.')
1237
1238 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001239 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001240 try:
maruel1ceb3872015-10-14 06:10:44 -07001241 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001242 data = json.load(f)
1243 except (IOError, ValueError):
1244 parser.error('Failed to open %s' % options.json)
1245 try:
1246 tasks = sorted(
1247 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1248 args = [t['task_id'] for t in tasks]
1249 except (KeyError, TypeError):
1250 parser.error('Failed to process %s' % options.json)
1251 if options.timeout is None:
1252 options.timeout = (
1253 data['request']['properties']['execution_timeout_secs'] +
1254 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001255 else:
1256 valid = frozenset('0123456789abcdef')
1257 if any(not valid.issuperset(task_id) for task_id in args):
1258 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001259
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001260 try:
1261 return collect(
1262 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001263 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001264 options.timeout,
1265 options.decorate,
1266 options.print_status_updates,
1267 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001268 options.task_output_dir,
1269 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001270 except Failure:
1271 on_error.report(None)
1272 return 1
1273
1274
maruelbea00862015-09-18 09:55:36 -07001275@subcommand.usage('[filename]')
1276def CMDput_bootstrap(parser, args):
1277 """Uploads a new version of bootstrap.py."""
1278 options, args = parser.parse_args(args)
1279 if len(args) != 1:
1280 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001281 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001282 path = unicode(os.path.abspath(args[0]))
1283 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001284 content = f.read().decode('utf-8')
1285 data = net.url_read_json(url, data={'content': content})
1286 print data
1287 return 0
1288
1289
1290@subcommand.usage('[filename]')
1291def CMDput_bot_config(parser, args):
1292 """Uploads a new version of bot_config.py."""
1293 options, args = parser.parse_args(args)
1294 if len(args) != 1:
1295 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001296 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001297 path = unicode(os.path.abspath(args[0]))
1298 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001299 content = f.read().decode('utf-8')
1300 data = net.url_read_json(url, data={'content': content})
1301 print data
1302 return 0
1303
1304
maruel77f720b2015-09-15 12:35:22 -07001305@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001306def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001307 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1308 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001309
1310 Examples:
maruel77f720b2015-09-15 12:35:22 -07001311 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001312 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001313
maruel77f720b2015-09-15 12:35:22 -07001314 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001315 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1316
1317 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1318 quoting is important!:
1319 swarming.py query -S server-url.com --limit 10 \\
1320 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001321 """
1322 CHUNK_SIZE = 250
1323
1324 parser.add_option(
1325 '-L', '--limit', type='int', default=200,
1326 help='Limit to enforce on limitless items (like number of tasks); '
1327 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001328 parser.add_option(
1329 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001330 parser.add_option(
1331 '--progress', action='store_true',
1332 help='Prints a dot at each request to show progress')
1333 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001334 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001335 parser.error(
1336 'Must specify only method name and optionally query args properly '
1337 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001338 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001339 url = base_url
1340 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001341 # Check check, change if not working out.
1342 merge_char = '&' if '?' in url else '?'
1343 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001344 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001345 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001346 # TODO(maruel): Do basic diagnostic.
1347 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001348 return 1
1349
1350 # Some items support cursors. Try to get automatically if cursors are needed
1351 # by looking at the 'cursor' items.
1352 while (
1353 data.get('cursor') and
1354 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001355 merge_char = '&' if '?' in base_url else '?'
1356 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001357 if options.limit:
1358 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001359 if options.progress:
1360 sys.stdout.write('.')
1361 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001362 new = net.url_read_json(url)
1363 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001364 if options.progress:
1365 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001366 print >> sys.stderr, 'Failed to access %s' % options.swarming
1367 return 1
maruel81b37132015-10-21 06:42:13 -07001368 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001369 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001370
maruel77f720b2015-09-15 12:35:22 -07001371 if options.progress:
1372 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001373 if options.limit and len(data.get('items', [])) > options.limit:
1374 data['items'] = data['items'][:options.limit]
1375 data.pop('cursor', None)
1376
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001377 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001378 options.json = unicode(os.path.abspath(options.json))
1379 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001380 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001381 try:
maruel77f720b2015-09-15 12:35:22 -07001382 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001383 sys.stdout.write('\n')
1384 except IOError:
1385 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001386 return 0
1387
1388
maruel77f720b2015-09-15 12:35:22 -07001389def CMDquery_list(parser, args):
1390 """Returns list of all the Swarming APIs that can be used with command
1391 'query'.
1392 """
1393 parser.add_option(
1394 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1395 options, args = parser.parse_args(args)
1396 if args:
1397 parser.error('No argument allowed.')
1398
1399 try:
1400 apis = endpoints_api_discovery_apis(options.swarming)
1401 except APIError as e:
1402 parser.error(str(e))
1403 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001404 options.json = unicode(os.path.abspath(options.json))
1405 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001406 json.dump(apis, f)
1407 else:
1408 help_url = (
1409 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1410 options.swarming)
1411 for api_id, api in sorted(apis.iteritems()):
1412 print api_id
1413 print ' ' + api['description']
1414 for resource_name, resource in sorted(api['resources'].iteritems()):
1415 print ''
1416 for method_name, method in sorted(resource['methods'].iteritems()):
1417 # Only list the GET ones.
1418 if method['httpMethod'] != 'GET':
1419 continue
1420 print '- %s.%s: %s' % (
1421 resource_name, method_name, method['path'])
1422 print ' ' + method['description']
1423 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1424 return 0
1425
1426
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001427@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001428def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001429 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001430
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001431 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001432 """
1433 add_trigger_options(parser)
1434 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001435 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001436 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001437 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001438 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001439 tasks = trigger_task_shards(
1440 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001441 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001442 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001443 'Failed to trigger %s(%s): %s' %
1444 (options.task_name, args[0], e.args[0]))
1445 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001446 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001447 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001448 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001449 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001450 task_ids = [
1451 t['task_id']
1452 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1453 ]
maruel71c61c82016-02-22 06:52:05 -08001454 if options.timeout is None:
1455 options.timeout = (
1456 task_request.properties.execution_timeout_secs +
1457 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001458 try:
1459 return collect(
1460 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001461 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001462 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001463 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001464 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001465 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001466 options.task_output_dir,
1467 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001468 except Failure:
1469 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001470 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001471
1472
maruel18122c62015-10-23 06:31:23 -07001473@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001474def CMDreproduce(parser, args):
1475 """Runs a task locally that was triggered on the server.
1476
1477 This running locally the same commands that have been run on the bot. The data
1478 downloaded will be in a subdirectory named 'work' of the current working
1479 directory.
maruel18122c62015-10-23 06:31:23 -07001480
1481 You can pass further additional arguments to the target command by passing
1482 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001483 """
maruelc070e672016-02-22 17:32:57 -08001484 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001485 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001486 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001487 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001488 extra_args = []
1489 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001490 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001491 if len(args) > 1:
1492 if args[1] == '--':
1493 if len(args) > 2:
1494 extra_args = args[2:]
1495 else:
1496 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001497
maruel380e3262016-08-31 16:10:06 -07001498 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001499 request = net.url_read_json(url)
1500 if not request:
1501 print >> sys.stderr, 'Failed to retrieve request data for the task'
1502 return 1
1503
maruel12e30012015-10-09 11:55:35 -07001504 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001505 if fs.isdir(workdir):
1506 parser.error('Please delete the directory \'work\' first')
1507 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001508
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001509 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001510 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001511 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001512 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001513 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001514 for i in properties['env']:
1515 key = i['key'].encode('utf-8')
1516 if not i['value']:
1517 env.pop(key, None)
1518 else:
1519 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001520
nodir152cba62016-05-12 16:08:56 -07001521 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001522 # Create the tree.
1523 with isolateserver.get_storage(
1524 properties['inputs_ref']['isolatedserver'],
1525 properties['inputs_ref']['namespace']) as storage:
1526 bundle = isolateserver.fetch_isolated(
1527 properties['inputs_ref']['isolated'],
1528 storage,
1529 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001530 workdir,
1531 False)
maruel29ab2fd2015-10-16 11:44:01 -07001532 command = bundle.command
1533 if bundle.relative_cwd:
1534 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001535 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001536 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001537 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001538 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001539 if not options.output_dir and new_command != command:
1540 parser.error('The task has outputs, you must use --output-dir')
1541 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001542 else:
1543 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001544 try:
maruel18122c62015-10-23 06:31:23 -07001545 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001546 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001547 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001548 print >> sys.stderr, str(e)
1549 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001550
1551
maruel0eb1d1b2015-10-02 14:48:21 -07001552@subcommand.usage('bot_id')
1553def CMDterminate(parser, args):
1554 """Tells a bot to gracefully shut itself down as soon as it can.
1555
1556 This is done by completing whatever current task there is then exiting the bot
1557 process.
1558 """
1559 parser.add_option(
1560 '--wait', action='store_true', help='Wait for the bot to terminate')
1561 options, args = parser.parse_args(args)
1562 if len(args) != 1:
1563 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001564 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001565 request = net.url_read_json(url, data={})
1566 if not request:
1567 print >> sys.stderr, 'Failed to ask for termination'
1568 return 1
1569 if options.wait:
1570 return collect(
maruel9531ce02016-04-13 06:11:23 -07001571 options.swarming, [request['task_id']], 0., False, False, None, None,
1572 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001573 return 0
1574
1575
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001576@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001577def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001578 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001579
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001580 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001581 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001582
1583 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001584
1585 Passes all extra arguments provided after '--' as additional command line
1586 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001587 """
1588 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001589 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001590 parser.add_option(
1591 '--dump-json',
1592 metavar='FILE',
1593 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001594 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001595 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001596 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001597 tasks = trigger_task_shards(
1598 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001599 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001600 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001601 tasks_sorted = sorted(
1602 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001603 if options.dump_json:
1604 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001605 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001606 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001607 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001608 }
maruel46b015f2015-10-13 18:40:35 -07001609 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001610 print('To collect results, use:')
1611 print(' swarming.py collect -S %s --json %s' %
1612 (options.swarming, options.dump_json))
1613 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001614 print('To collect results, use:')
1615 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001616 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1617 print('Or visit:')
1618 for t in tasks_sorted:
1619 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001620 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001621 except Failure:
1622 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001623 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001624
1625
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001626class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001627 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001628 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001629 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001630 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001631 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001632 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001633 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001634 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001635 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001636 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001637
1638 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001639 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001640 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001641 auth.process_auth_options(self, options)
1642 user = self._process_swarming(options)
1643 if hasattr(options, 'user') and not options.user:
1644 options.user = user
1645 return options, args
1646
1647 def _process_swarming(self, options):
1648 """Processes the --swarming option and aborts if not specified.
1649
1650 Returns the identity as determined by the server.
1651 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001652 if not options.swarming:
1653 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001654 try:
1655 options.swarming = net.fix_url(options.swarming)
1656 except ValueError as e:
1657 self.error('--swarming %s' % e)
1658 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001659 try:
1660 user = auth.ensure_logged_in(options.swarming)
1661 except ValueError as e:
1662 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001663 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001664
1665
1666def main(args):
1667 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001668 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001669
1670
1671if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001672 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001673 fix_encoding.fix_encoding()
1674 tools.disable_buffering()
1675 colorama.init()
1676 sys.exit(main(sys.argv[1:]))