blob: 2753a8c604e5faea7cc0071d74726c573c3183cf [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',
maruel77f720b2015-09-15 12:35:22 -0700179 ])
180
181
182# See ../appengine/swarming/swarming_rpcs.py.
183NewTaskRequest = collections.namedtuple(
184 'NewTaskRequest',
185 [
186 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500187 'name',
maruel77f720b2015-09-15 12:35:22 -0700188 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500189 'priority',
maruel77f720b2015-09-15 12:35:22 -0700190 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700191 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500192 'tags',
193 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500194 ])
195
196
maruel77f720b2015-09-15 12:35:22 -0700197def namedtuple_to_dict(value):
198 """Recursively converts a namedtuple to a dict."""
199 out = dict(value._asdict())
200 for k, v in out.iteritems():
201 if hasattr(v, '_asdict'):
202 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700203 elif isinstance(v, (list, tuple)):
204 l = []
205 for elem in v:
206 if hasattr(elem, '_asdict'):
207 l.append(namedtuple_to_dict(elem))
208 else:
209 l.append(elem)
210 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700211 return out
212
213
vadimsh93d167c2016-09-13 11:31:51 -0700214def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800215 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700216
217 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500218 """
maruel77f720b2015-09-15 12:35:22 -0700219 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700220 if hide_token:
221 if out['service_account_token'] not in (None, 'bot', 'none'):
222 out['service_account_token'] = '<hidden>'
223 # Don't send 'service_account_token' if it is None to avoid confusing older
224 # version of the server that doesn't know about 'service_account_token'.
225 if out['service_account_token'] in (None, 'none'):
226 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700227 # Maps are not supported until protobuf v3.
228 out['properties']['dimensions'] = [
229 {'key': k, 'value': v}
230 for k, v in out['properties']['dimensions'].iteritems()
231 ]
232 out['properties']['dimensions'].sort(key=lambda x: x['key'])
233 out['properties']['env'] = [
234 {'key': k, 'value': v}
235 for k, v in out['properties']['env'].iteritems()
236 ]
237 out['properties']['env'].sort(key=lambda x: x['key'])
238 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500239
240
maruel77f720b2015-09-15 12:35:22 -0700241def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242 """Triggers a request on the Swarming server and returns the json data.
243
244 It's the low-level function.
245
246 Returns:
247 {
248 'request': {
249 'created_ts': u'2010-01-02 03:04:05',
250 'name': ..
251 },
252 'task_id': '12300',
253 }
254 """
255 logging.info('Triggering: %s', raw_request['name'])
256
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500257 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700258 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500259 if not result:
260 on_error.report('Failed to trigger task %s' % raw_request['name'])
261 return None
maruele557bce2015-11-17 09:01:27 -0800262 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800263 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800264 msg = 'Failed to trigger task %s' % raw_request['name']
265 if result['error'].get('errors'):
266 for err in result['error']['errors']:
267 if err.get('message'):
268 msg += '\nMessage: %s' % err['message']
269 if err.get('debugInfo'):
270 msg += '\nDebug info:\n%s' % err['debugInfo']
271 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800272 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800273
274 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800275 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500276 return result
277
278
279def setup_googletest(env, shards, index):
280 """Sets googletest specific environment variables."""
281 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700282 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
283 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
284 env = env[:]
285 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
286 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500287 return env
288
289
290def trigger_task_shards(swarming, task_request, shards):
291 """Triggers one or many subtasks of a sharded task.
292
293 Returns:
294 Dict with task details, returned to caller as part of --dump-json output.
295 None in case of failure.
296 """
297 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700298 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500299 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700300 req['properties']['env'] = setup_googletest(
301 req['properties']['env'], shards, index)
302 req['name'] += ':%s:%s' % (index, shards)
303 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500304
305 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500306 tasks = {}
307 priority_warning = False
308 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700309 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500310 if not task:
311 break
312 logging.info('Request result: %s', task)
313 if (not priority_warning and
314 task['request']['priority'] != task_request.priority):
315 priority_warning = True
316 print >> sys.stderr, (
317 'Priority was reset to %s' % task['request']['priority'])
318 tasks[request['name']] = {
319 'shard_index': index,
320 'task_id': task['task_id'],
321 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
322 }
323
324 # Some shards weren't triggered. Abort everything.
325 if len(tasks) != len(requests):
326 if tasks:
327 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
328 len(tasks), len(requests))
329 for task_dict in tasks.itervalues():
330 abort_task(swarming, task_dict['task_id'])
331 return None
332
333 return tasks
334
335
vadimsh93d167c2016-09-13 11:31:51 -0700336def mint_service_account_token(service_account):
337 """Given a service account name returns a delegation token for this account.
338
339 The token is generated based on triggering user's credentials. It is passed
340 to Swarming, that uses it when running tasks.
341 """
342 logging.info(
343 'Generating delegation token for service account "%s"', service_account)
344 raise NotImplementedError('Custom service accounts are not implemented yet')
345
346
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500347### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000348
349
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700350# How often to print status updates to stdout in 'collect'.
351STATUS_UPDATE_INTERVAL = 15 * 60.
352
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400353
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400354class State(object):
355 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000356
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400357 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
358 values are part of the API so if they change, the API changed.
359
360 It's in fact an enum. Values should be in decreasing order of importance.
361 """
362 RUNNING = 0x10
363 PENDING = 0x20
364 EXPIRED = 0x30
365 TIMED_OUT = 0x40
366 BOT_DIED = 0x50
367 CANCELED = 0x60
368 COMPLETED = 0x70
369
maruel77f720b2015-09-15 12:35:22 -0700370 STATES = (
371 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
372 'COMPLETED')
373 STATES_RUNNING = ('RUNNING', 'PENDING')
374 STATES_NOT_RUNNING = (
375 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
376 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
377 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400378
379 _NAMES = {
380 RUNNING: 'Running',
381 PENDING: 'Pending',
382 EXPIRED: 'Expired',
383 TIMED_OUT: 'Execution timed out',
384 BOT_DIED: 'Bot died',
385 CANCELED: 'User canceled',
386 COMPLETED: 'Completed',
387 }
388
maruel77f720b2015-09-15 12:35:22 -0700389 _ENUMS = {
390 'RUNNING': RUNNING,
391 'PENDING': PENDING,
392 'EXPIRED': EXPIRED,
393 'TIMED_OUT': TIMED_OUT,
394 'BOT_DIED': BOT_DIED,
395 'CANCELED': CANCELED,
396 'COMPLETED': COMPLETED,
397 }
398
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400399 @classmethod
400 def to_string(cls, state):
401 """Returns a user-readable string representing a State."""
402 if state not in cls._NAMES:
403 raise ValueError('Invalid state %s' % state)
404 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000405
maruel77f720b2015-09-15 12:35:22 -0700406 @classmethod
407 def from_enum(cls, state):
408 """Returns int value based on the string."""
409 if state not in cls._ENUMS:
410 raise ValueError('Invalid state %s' % state)
411 return cls._ENUMS[state]
412
maruel@chromium.org0437a732013-08-27 16:05:52 +0000413
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700414class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700415 """Assembles task execution summary (for --task-summary-json output).
416
417 Optionally fetches task outputs from isolate server to local disk (used when
418 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700419
420 This object is shared among multiple threads running 'retrieve_results'
421 function, in particular they call 'process_shard_result' method in parallel.
422 """
423
maruel0eb1d1b2015-10-02 14:48:21 -0700424 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700425 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
426
427 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700428 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700429 shard_count: expected number of task shards.
430 """
maruel12e30012015-10-09 11:55:35 -0700431 self.task_output_dir = (
432 unicode(os.path.abspath(task_output_dir))
433 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700434 self.shard_count = shard_count
435
436 self._lock = threading.Lock()
437 self._per_shard_results = {}
438 self._storage = None
439
nodire5028a92016-04-29 14:38:21 -0700440 if self.task_output_dir:
441 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442
Vadim Shtayurab450c602014-05-12 19:23:25 -0700443 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444 """Stores results of a single task shard, fetches output files if necessary.
445
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400446 Modifies |result| in place.
447
maruel77f720b2015-09-15 12:35:22 -0700448 shard_index is 0-based.
449
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700450 Called concurrently from multiple threads.
451 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700453 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 if shard_index < 0 or shard_index >= self.shard_count:
455 logging.warning(
456 'Shard index %d is outside of expected range: [0; %d]',
457 shard_index, self.shard_count - 1)
458 return
459
maruel77f720b2015-09-15 12:35:22 -0700460 if result.get('outputs_ref'):
461 ref = result['outputs_ref']
462 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
463 ref['isolatedserver'],
464 urllib.urlencode(
465 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400466
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700467 # Store result dict of that shard, ignore results we've already seen.
468 with self._lock:
469 if shard_index in self._per_shard_results:
470 logging.warning('Ignoring duplicate shard index %d', shard_index)
471 return
472 self._per_shard_results[shard_index] = result
473
474 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700475 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400476 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700477 result['outputs_ref']['isolatedserver'],
478 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400479 if storage:
480 # Output files are supposed to be small and they are not reused across
481 # tasks. So use MemoryCache for them instead of on-disk cache. Make
482 # files writable, so that calling script can delete them.
483 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700484 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400485 storage,
486 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700487 os.path.join(self.task_output_dir, str(shard_index)),
488 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700489
490 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700491 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700492 with self._lock:
493 # Write an array of shard results with None for missing shards.
494 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700495 'shards': [
496 self._per_shard_results.get(i) for i in xrange(self.shard_count)
497 ],
498 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700499 # Write summary.json to task_output_dir as well.
500 if self.task_output_dir:
501 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700502 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700503 summary,
504 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700505 if self._storage:
506 self._storage.close()
507 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700508 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700509
510 def _get_storage(self, isolate_server, namespace):
511 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700512 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700513 with self._lock:
514 if not self._storage:
515 self._storage = isolateserver.get_storage(isolate_server, namespace)
516 else:
517 # Shards must all use exact same isolate server and namespace.
518 if self._storage.location != isolate_server:
519 logging.error(
520 'Task shards are using multiple isolate servers: %s and %s',
521 self._storage.location, isolate_server)
522 return None
523 if self._storage.namespace != namespace:
524 logging.error(
525 'Task shards are using multiple namespaces: %s and %s',
526 self._storage.namespace, namespace)
527 return None
528 return self._storage
529
530
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500531def now():
532 """Exists so it can be mocked easily."""
533 return time.time()
534
535
maruel77f720b2015-09-15 12:35:22 -0700536def parse_time(value):
537 """Converts serialized time from the API to datetime.datetime."""
538 # When microseconds are 0, the '.123456' suffix is elided. This means the
539 # serialized format is not consistent, which confuses the hell out of python.
540 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
541 try:
542 return datetime.datetime.strptime(value, fmt)
543 except ValueError:
544 pass
545 raise ValueError('Failed to parse %s' % value)
546
547
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700548def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700549 base_url, shard_index, task_id, timeout, should_stop, output_collector,
550 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400551 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700552
Vadim Shtayurab450c602014-05-12 19:23:25 -0700553 Returns:
554 <result dict> on success.
555 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700556 """
maruel71c61c82016-02-22 06:52:05 -0800557 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700558 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700559 if include_perf:
560 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700561 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700562 started = now()
563 deadline = started + timeout if timeout else None
564 attempt = 0
565
566 while not should_stop.is_set():
567 attempt += 1
568
569 # Waiting for too long -> give up.
570 current_time = now()
571 if deadline and current_time >= deadline:
572 logging.error('retrieve_results(%s) timed out on attempt %d',
573 base_url, attempt)
574 return None
575
576 # Do not spin too fast. Spin faster at the beginning though.
577 # Start with 1 sec delay and for each 30 sec of waiting add another second
578 # of delay, until hitting 15 sec ceiling.
579 if attempt > 1:
580 max_delay = min(15, 1 + (current_time - started) / 30.0)
581 delay = min(max_delay, deadline - current_time) if deadline else max_delay
582 if delay > 0:
583 logging.debug('Waiting %.1f sec before retrying', delay)
584 should_stop.wait(delay)
585 if should_stop.is_set():
586 return None
587
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400588 # Disable internal retries in net.url_read_json, since we are doing retries
589 # ourselves.
590 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700591 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
592 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400593 result = net.url_read_json(result_url, retry_50x=False)
594 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400595 continue
maruel77f720b2015-09-15 12:35:22 -0700596
maruelbf53e042015-12-01 15:00:51 -0800597 if result.get('error'):
598 # An error occurred.
599 if result['error'].get('errors'):
600 for err in result['error']['errors']:
601 logging.warning(
602 'Error while reading task: %s; %s',
603 err.get('message'), err.get('debugInfo'))
604 elif result['error'].get('message'):
605 logging.warning(
606 'Error while reading task: %s', result['error']['message'])
607 continue
608
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400609 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700610 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400611 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700612 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700613 # Record the result, try to fetch attached output files (if any).
614 if output_collector:
615 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700616 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700617 if result.get('internal_failure'):
618 logging.error('Internal error!')
619 elif result['state'] == 'BOT_DIED':
620 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700621 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000622
623
maruel77f720b2015-09-15 12:35:22 -0700624def convert_to_old_format(result):
625 """Converts the task result data from Endpoints API format to old API format
626 for compatibility.
627
628 This goes into the file generated as --task-summary-json.
629 """
630 # Sets default.
631 result.setdefault('abandoned_ts', None)
632 result.setdefault('bot_id', None)
633 result.setdefault('bot_version', None)
634 result.setdefault('children_task_ids', [])
635 result.setdefault('completed_ts', None)
636 result.setdefault('cost_saved_usd', None)
637 result.setdefault('costs_usd', None)
638 result.setdefault('deduped_from', None)
639 result.setdefault('name', None)
640 result.setdefault('outputs_ref', None)
641 result.setdefault('properties_hash', None)
642 result.setdefault('server_versions', None)
643 result.setdefault('started_ts', None)
644 result.setdefault('tags', None)
645 result.setdefault('user', None)
646
647 # Convertion back to old API.
648 duration = result.pop('duration', None)
649 result['durations'] = [duration] if duration else []
650 exit_code = result.pop('exit_code', None)
651 result['exit_codes'] = [int(exit_code)] if exit_code else []
652 result['id'] = result.pop('task_id')
653 result['isolated_out'] = result.get('outputs_ref', None)
654 output = result.pop('output', None)
655 result['outputs'] = [output] if output else []
656 # properties_hash
657 # server_version
658 # Endpoints result 'state' as string. For compatibility with old code, convert
659 # to int.
660 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700661 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700662 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700663 if 'bot_dimensions' in result:
664 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700665 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700666 }
667 else:
668 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700669
670
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700671def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400672 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700673 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500674 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700676 Duplicate shards are ignored. Shards are yielded in order of completion.
677 Timed out shards are NOT yielded at all. Caller can compare number of yielded
678 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679
680 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500681 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 +0000682 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500683
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700684 output_collector is an optional instance of TaskOutputCollector that will be
685 used to fetch files produced by a task from isolate server to the local disk.
686
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500687 Yields:
688 (index, result). In particular, 'result' is defined as the
689 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000691 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400692 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700693 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700694 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700695
maruel@chromium.org0437a732013-08-27 16:05:52 +0000696 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
697 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700698 # Adds a task to the thread pool to call 'retrieve_results' and return
699 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400700 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700701 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000702 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400703 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700704 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700705
706 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400707 for shard_index, task_id in enumerate(task_ids):
708 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700709
710 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400711 shards_remaining = range(len(task_ids))
712 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700713 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700714 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700715 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700716 shard_index, result = results_channel.pull(
717 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700718 except threading_utils.TaskChannel.Timeout:
719 if print_status_updates:
720 print(
721 'Waiting for results from the following shards: %s' %
722 ', '.join(map(str, shards_remaining)))
723 sys.stdout.flush()
724 continue
725 except Exception:
726 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700727
728 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700729 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000730 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500731 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000732 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700733
Vadim Shtayurab450c602014-05-12 19:23:25 -0700734 # Yield back results to the caller.
735 assert shard_index in shards_remaining
736 shards_remaining.remove(shard_index)
737 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700738
maruel@chromium.org0437a732013-08-27 16:05:52 +0000739 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700740 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000741 should_stop.set()
742
743
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400744def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000745 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700746 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400747 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700748 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
749 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400750 else:
751 pending = 'N/A'
752
maruel77f720b2015-09-15 12:35:22 -0700753 if metadata.get('duration') is not None:
754 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400755 else:
756 duration = 'N/A'
757
maruel77f720b2015-09-15 12:35:22 -0700758 if metadata.get('exit_code') is not None:
759 # Integers are encoded as string to not loose precision.
760 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400761 else:
762 exit_code = 'N/A'
763
764 bot_id = metadata.get('bot_id') or 'N/A'
765
maruel77f720b2015-09-15 12:35:22 -0700766 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400767 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400768 tag_footer = (
769 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
770 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400771
772 tag_len = max(len(tag_header), len(tag_footer))
773 dash_pad = '+-%s-+\n' % ('-' * tag_len)
774 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
775 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
776
777 header = dash_pad + tag_header + dash_pad
778 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700779 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400780 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000781
782
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700783def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700784 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700785 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700786 """Retrieves results of a Swarming task.
787
788 Returns:
789 process exit code that should be returned to the user.
790 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700791 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700792 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700793
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700794 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700795 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400796 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700797 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400798 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400799 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700800 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700801 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700802
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400803 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700804 shard_exit_code = metadata.get('exit_code')
805 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700806 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700807 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700808 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400809 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700810 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700811
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700812 if decorate:
leileied181762016-10-13 14:24:59 -0700813 s = decorate_shard_output(swarming, index, metadata).encode(
814 'utf-8', 'replace')
815 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400816 if len(seen_shards) < len(task_ids):
817 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700818 else:
maruel77f720b2015-09-15 12:35:22 -0700819 print('%s: %s %s' % (
820 metadata.get('bot_id', 'N/A'),
821 metadata['task_id'],
822 shard_exit_code))
823 if metadata['output']:
824 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400825 if output:
826 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700827 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700828 summary = output_collector.finalize()
829 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700830 # TODO(maruel): Make this optional.
831 for i in summary['shards']:
832 if i:
833 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700834 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700835
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400836 if decorate and total_duration:
837 print('Total duration: %.1fs' % total_duration)
838
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400839 if len(seen_shards) != len(task_ids):
840 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700841 print >> sys.stderr, ('Results from some shards are missing: %s' %
842 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700843 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700844
maruela5490782015-09-30 10:56:59 -0700845 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000846
847
maruel77f720b2015-09-15 12:35:22 -0700848### API management.
849
850
851class APIError(Exception):
852 pass
853
854
855def endpoints_api_discovery_apis(host):
856 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
857 the APIs exposed by a host.
858
859 https://developers.google.com/discovery/v1/reference/apis/list
860 """
maruel380e3262016-08-31 16:10:06 -0700861 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
862 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700863 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
864 if data is None:
865 raise APIError('Failed to discover APIs on %s' % host)
866 out = {}
867 for api in data['items']:
868 if api['id'] == 'discovery:v1':
869 continue
870 # URL is of the following form:
871 # url = host + (
872 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
873 api_data = net.url_read_json(api['discoveryRestUrl'])
874 if api_data is None:
875 raise APIError('Failed to discover %s on %s' % (api['id'], host))
876 out[api['id']] = api_data
877 return out
878
879
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500880### Commands.
881
882
883def abort_task(_swarming, _manifest):
884 """Given a task manifest that was triggered, aborts its execution."""
885 # TODO(vadimsh): No supported by the server yet.
886
887
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400888def add_filter_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400889 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500890 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500891 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500892 dest='dimensions', metavar='FOO bar',
893 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500894 parser.add_option_group(parser.filter_group)
895
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400896
Vadim Shtayurab450c602014-05-12 19:23:25 -0700897def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400898 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700899 parser.sharding_group.add_option(
900 '--shards', type='int', default=1,
901 help='Number of shards to trigger and collect.')
902 parser.add_option_group(parser.sharding_group)
903
904
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400905def add_trigger_options(parser):
906 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500907 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400908 add_filter_options(parser)
909
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400910 parser.task_group = optparse.OptionGroup(parser, 'Task properties')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500911 parser.task_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500912 '-s', '--isolated',
913 help='Hash of the .isolated to grab from the isolate server')
914 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500915 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700916 help='Environment variables to set')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500917 parser.task_group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500918 '--priority', type='int', default=100,
919 help='The lower value, the more important the task is')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500920 parser.task_group.add_option(
Marc-Antoine Ruel5b475782014-02-14 20:57:59 -0500921 '-T', '--task-name',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400922 help='Display name of the task. Defaults to '
923 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
924 'isolated file is provided, if a hash is provided, it defaults to '
925 '<user>/<dimensions>/<isolated hash>/<timestamp>')
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400926 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400927 '--tags', action='append', default=[],
928 help='Tags to assign to the task.')
929 parser.task_group.add_option(
Marc-Antoine Ruel686a2872014-12-05 10:06:29 -0500930 '--user', default='',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400931 help='User associated with the task. Defaults to authenticated user on '
932 'the server.')
933 parser.task_group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400934 '--idempotent', action='store_true', default=False,
935 help='When set, the server will actively try to find a previous task '
936 'with the same parameter and return this result instead if possible')
937 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400938 '--expiration', type='int', default=6*60*60,
Marc-Antoine Ruel13b7b782014-03-14 11:14:57 -0400939 help='Seconds to allow the task to be pending for a bot to run before '
940 'this task request expires.')
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400941 parser.task_group.add_option(
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400942 '--deadline', type='int', dest='expiration',
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400943 help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruel77142812014-10-03 11:19:43 -0400944 parser.task_group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400945 '--hard-timeout', type='int', default=60*60,
946 help='Seconds to allow the task to complete.')
947 parser.task_group.add_option(
948 '--io-timeout', type='int', default=20*60,
949 help='Seconds to allow the task to be silent.')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500950 parser.task_group.add_option(
951 '--raw-cmd', action='store_true', default=False,
952 help='When set, the command after -- is used as-is without run_isolated. '
953 'In this case, no .isolated file is expected.')
borenet02f772b2016-06-22 12:42:19 -0700954 parser.task_group.add_option(
955 '--cipd-package', action='append', default=[],
956 help='CIPD packages to install on the Swarming bot. Uses the format: '
957 'path:package_name:version')
vadimsh93d167c2016-09-13 11:31:51 -0700958 parser.task_group.add_option(
959 '--service-account',
960 help='Name of a service account to run the task as. Only literal "bot" '
961 'string can be specified currently (to run the task under bot\'s '
962 'account). Don\'t use task service accounts if not given '
963 '(default).')
aludwincc5524e2016-10-28 10:25:24 -0700964 parser.task_group.add_option(
965 '-o', '--output', action='append', default=[],
966 help='A list of files to return in addition to those written to'
967 '$(ISOLATED_OUTDIR). An error will occur if a file specified by'
968 'this option is also written directly to $(ISOLATED_OUTDIR).')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500969 parser.add_option_group(parser.task_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000970
971
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500972def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700973 """Processes trigger options and does preparatory steps.
974
975 Uploads files to isolate server and generates service account tokens if
976 necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500977 """
978 options.dimensions = dict(options.dimensions)
979 options.env = dict(options.env)
980
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500981 if not options.dimensions:
982 parser.error('Please at least specify one --dimension')
983 if options.raw_cmd:
984 if not args:
985 parser.error(
986 'Arguments with --raw-cmd should be passed after -- as command '
987 'delimiter.')
988 if options.isolate_server:
989 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
990
991 command = args
992 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500993 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500994 options.user,
995 '_'.join(
996 '%s=%s' % (k, v)
997 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700998 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500999 else:
nodir55be77b2016-05-03 09:39:57 -07001000 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001001 try:
maruel77f720b2015-09-15 12:35:22 -07001002 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001003 except ValueError as e:
1004 parser.error(str(e))
1005
borenet02f772b2016-06-22 12:42:19 -07001006 cipd_packages = []
1007 for p in options.cipd_package:
1008 split = p.split(':', 2)
1009 if len(split) != 3:
1010 parser.error('CIPD packages must take the form: path:package:version')
1011 cipd_packages.append(CipdPackage(
1012 package_name=split[1],
1013 path=split[0],
1014 version=split[2]))
1015 cipd_input = None
1016 if cipd_packages:
1017 cipd_input = CipdInput(
1018 client_package=None,
1019 packages=cipd_packages,
1020 server=None)
1021
nodir152cba62016-05-12 16:08:56 -07001022 # If inputs_ref.isolated is used, command is actually extra_args.
1023 # Otherwise it's an actual command to run.
1024 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -07001025 properties = TaskProperties(
borenet02f772b2016-06-22 12:42:19 -07001026 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -07001027 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001028 dimensions=options.dimensions,
1029 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001030 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -07001031 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -07001032 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001033 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001034 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001035 io_timeout_secs=options.io_timeout,
1036 outputs=options.output)
maruel8fce7962015-10-21 11:17:47 -07001037 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1038 parser.error('--tags must be in the format key:value')
vadimsh93d167c2016-09-13 11:31:51 -07001039
1040 # Convert a service account email to a signed service account token to pass
1041 # to Swarming.
1042 service_account_token = None
1043 if options.service_account in ('bot', 'none'):
1044 service_account_token = options.service_account
1045 elif options.service_account:
1046 # pylint: disable=assignment-from-no-return
1047 service_account_token = mint_service_account_token(options.service_account)
1048
maruel77f720b2015-09-15 12:35:22 -07001049 return NewTaskRequest(
1050 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001051 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001052 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001053 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001054 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001055 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001056 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001057 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001058
1059
1060def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001061 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001062 '-t', '--timeout', type='float',
1063 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1064 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001065 parser.group_logging.add_option(
1066 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001067 parser.group_logging.add_option(
1068 '--print-status-updates', action='store_true',
1069 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001070 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001071 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001072 '--task-summary-json',
1073 metavar='FILE',
1074 help='Dump a summary of task results to this file as json. It contains '
1075 'only shards statuses as know to server directly. Any output files '
1076 'emitted by the task can be collected by using --task-output-dir')
1077 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001078 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001079 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001080 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001081 'directory contains per-shard directory with output files produced '
1082 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001083 parser.task_output_group.add_option(
1084 '--perf', action='store_true', default=False,
1085 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001086 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001087
1088
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001089@subcommand.usage('bots...')
1090def CMDbot_delete(parser, args):
1091 """Forcibly deletes bots from the Swarming server."""
1092 parser.add_option(
1093 '-f', '--force', action='store_true',
1094 help='Do not prompt for confirmation')
1095 options, args = parser.parse_args(args)
1096 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001097 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001098
1099 bots = sorted(args)
1100 if not options.force:
1101 print('Delete the following bots?')
1102 for bot in bots:
1103 print(' %s' % bot)
1104 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1105 print('Goodbye.')
1106 return 1
1107
1108 result = 0
1109 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001110 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001111 if net.url_read_json(url, data={}, method='POST') is None:
1112 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001113 result = 1
1114 return result
1115
1116
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001117def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001118 """Returns information about the bots connected to the Swarming server."""
1119 add_filter_options(parser)
1120 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001121 '--dead-only', action='store_true',
1122 help='Only print dead bots, useful to reap them and reimage broken bots')
1123 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001124 '-k', '--keep-dead', action='store_true',
1125 help='Do not filter out dead bots')
1126 parser.filter_group.add_option(
1127 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001128 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001129 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001130
1131 if options.keep_dead and options.dead_only:
1132 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001133
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001134 bots = []
1135 cursor = None
1136 limit = 250
1137 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001138 base_url = (
maruel380e3262016-08-31 16:10:06 -07001139 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001140 while True:
1141 url = base_url
1142 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001143 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001144 data = net.url_read_json(url)
1145 if data is None:
1146 print >> sys.stderr, 'Failed to access %s' % options.swarming
1147 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001148 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001149 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001150 if not cursor:
1151 break
1152
maruel77f720b2015-09-15 12:35:22 -07001153 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001154 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001155 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001156 continue
maruel77f720b2015-09-15 12:35:22 -07001157 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001158 continue
1159
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001160 # If the user requested to filter on dimensions, ensure the bot has all the
1161 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001162 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001163 for key, value in options.dimensions:
1164 if key not in dimensions:
1165 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001166 # A bot can have multiple value for a key, for example,
1167 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1168 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001169 if isinstance(dimensions[key], list):
1170 if value not in dimensions[key]:
1171 break
1172 else:
1173 if value != dimensions[key]:
1174 break
1175 else:
maruel77f720b2015-09-15 12:35:22 -07001176 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001177 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001178 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001179 if bot.get('task_id'):
1180 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001181 return 0
1182
1183
maruelfd0a90c2016-06-10 11:51:10 -07001184@subcommand.usage('task_id')
1185def CMDcancel(parser, args):
1186 """Cancels a task."""
1187 options, args = parser.parse_args(args)
1188 if not args:
1189 parser.error('Please specify the task to cancel')
1190 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001191 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001192 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1193 print('Deleting %s failed. Probably already gone' % task_id)
1194 return 1
1195 return 0
1196
1197
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001198@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001199def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001200 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001201
1202 The result can be in multiple part if the execution was sharded. It can
1203 potentially have retries.
1204 """
1205 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001206 parser.add_option(
1207 '-j', '--json',
1208 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001209 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001210 if not args and not options.json:
1211 parser.error('Must specify at least one task id or --json.')
1212 if args and options.json:
1213 parser.error('Only use one of task id or --json.')
1214
1215 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001216 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001217 try:
maruel1ceb3872015-10-14 06:10:44 -07001218 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001219 data = json.load(f)
1220 except (IOError, ValueError):
1221 parser.error('Failed to open %s' % options.json)
1222 try:
1223 tasks = sorted(
1224 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1225 args = [t['task_id'] for t in tasks]
1226 except (KeyError, TypeError):
1227 parser.error('Failed to process %s' % options.json)
1228 if options.timeout is None:
1229 options.timeout = (
1230 data['request']['properties']['execution_timeout_secs'] +
1231 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001232 else:
1233 valid = frozenset('0123456789abcdef')
1234 if any(not valid.issuperset(task_id) for task_id in args):
1235 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001236
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001237 try:
1238 return collect(
1239 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001240 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001241 options.timeout,
1242 options.decorate,
1243 options.print_status_updates,
1244 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001245 options.task_output_dir,
1246 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001247 except Failure:
1248 on_error.report(None)
1249 return 1
1250
1251
maruelbea00862015-09-18 09:55:36 -07001252@subcommand.usage('[filename]')
1253def CMDput_bootstrap(parser, args):
1254 """Uploads a new version of bootstrap.py."""
1255 options, args = parser.parse_args(args)
1256 if len(args) != 1:
1257 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001258 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001259 path = unicode(os.path.abspath(args[0]))
1260 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001261 content = f.read().decode('utf-8')
1262 data = net.url_read_json(url, data={'content': content})
1263 print data
1264 return 0
1265
1266
1267@subcommand.usage('[filename]')
1268def CMDput_bot_config(parser, args):
1269 """Uploads a new version of bot_config.py."""
1270 options, args = parser.parse_args(args)
1271 if len(args) != 1:
1272 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001273 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001274 path = unicode(os.path.abspath(args[0]))
1275 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001276 content = f.read().decode('utf-8')
1277 data = net.url_read_json(url, data={'content': content})
1278 print data
1279 return 0
1280
1281
maruel77f720b2015-09-15 12:35:22 -07001282@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001283def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001284 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1285 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001286
1287 Examples:
maruel77f720b2015-09-15 12:35:22 -07001288 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001289 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001290
maruel77f720b2015-09-15 12:35:22 -07001291 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001292 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1293
1294 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1295 quoting is important!:
1296 swarming.py query -S server-url.com --limit 10 \\
1297 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001298 """
1299 CHUNK_SIZE = 250
1300
1301 parser.add_option(
1302 '-L', '--limit', type='int', default=200,
1303 help='Limit to enforce on limitless items (like number of tasks); '
1304 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001305 parser.add_option(
1306 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001307 parser.add_option(
1308 '--progress', action='store_true',
1309 help='Prints a dot at each request to show progress')
1310 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001311 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001312 parser.error(
1313 'Must specify only method name and optionally query args properly '
1314 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001315 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001316 url = base_url
1317 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001318 # Check check, change if not working out.
1319 merge_char = '&' if '?' in url else '?'
1320 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001321 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001322 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001323 # TODO(maruel): Do basic diagnostic.
1324 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001325 return 1
1326
1327 # Some items support cursors. Try to get automatically if cursors are needed
1328 # by looking at the 'cursor' items.
1329 while (
1330 data.get('cursor') and
1331 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001332 merge_char = '&' if '?' in base_url else '?'
1333 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001334 if options.limit:
1335 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001336 if options.progress:
1337 sys.stdout.write('.')
1338 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001339 new = net.url_read_json(url)
1340 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001341 if options.progress:
1342 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001343 print >> sys.stderr, 'Failed to access %s' % options.swarming
1344 return 1
maruel81b37132015-10-21 06:42:13 -07001345 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001346 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001347
maruel77f720b2015-09-15 12:35:22 -07001348 if options.progress:
1349 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001350 if options.limit and len(data.get('items', [])) > options.limit:
1351 data['items'] = data['items'][:options.limit]
1352 data.pop('cursor', None)
1353
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001354 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001355 options.json = unicode(os.path.abspath(options.json))
1356 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001357 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001358 try:
maruel77f720b2015-09-15 12:35:22 -07001359 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001360 sys.stdout.write('\n')
1361 except IOError:
1362 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001363 return 0
1364
1365
maruel77f720b2015-09-15 12:35:22 -07001366def CMDquery_list(parser, args):
1367 """Returns list of all the Swarming APIs that can be used with command
1368 'query'.
1369 """
1370 parser.add_option(
1371 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1372 options, args = parser.parse_args(args)
1373 if args:
1374 parser.error('No argument allowed.')
1375
1376 try:
1377 apis = endpoints_api_discovery_apis(options.swarming)
1378 except APIError as e:
1379 parser.error(str(e))
1380 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001381 options.json = unicode(os.path.abspath(options.json))
1382 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001383 json.dump(apis, f)
1384 else:
1385 help_url = (
1386 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1387 options.swarming)
1388 for api_id, api in sorted(apis.iteritems()):
1389 print api_id
1390 print ' ' + api['description']
1391 for resource_name, resource in sorted(api['resources'].iteritems()):
1392 print ''
1393 for method_name, method in sorted(resource['methods'].iteritems()):
1394 # Only list the GET ones.
1395 if method['httpMethod'] != 'GET':
1396 continue
1397 print '- %s.%s: %s' % (
1398 resource_name, method_name, method['path'])
1399 print ' ' + method['description']
1400 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1401 return 0
1402
1403
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001404@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001405def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001406 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001407
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001408 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001409 """
1410 add_trigger_options(parser)
1411 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001412 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001413 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001414 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001415 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001416 tasks = trigger_task_shards(
1417 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001418 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001419 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001420 'Failed to trigger %s(%s): %s' %
1421 (options.task_name, args[0], e.args[0]))
1422 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001423 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001424 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001425 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001426 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001427 task_ids = [
1428 t['task_id']
1429 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1430 ]
maruel71c61c82016-02-22 06:52:05 -08001431 if options.timeout is None:
1432 options.timeout = (
1433 task_request.properties.execution_timeout_secs +
1434 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001435 try:
1436 return collect(
1437 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001438 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001439 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001440 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001441 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001442 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001443 options.task_output_dir,
1444 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001445 except Failure:
1446 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001447 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001448
1449
maruel18122c62015-10-23 06:31:23 -07001450@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001451def CMDreproduce(parser, args):
1452 """Runs a task locally that was triggered on the server.
1453
1454 This running locally the same commands that have been run on the bot. The data
1455 downloaded will be in a subdirectory named 'work' of the current working
1456 directory.
maruel18122c62015-10-23 06:31:23 -07001457
1458 You can pass further additional arguments to the target command by passing
1459 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001460 """
maruelc070e672016-02-22 17:32:57 -08001461 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001462 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001463 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001464 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001465 extra_args = []
1466 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001467 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001468 if len(args) > 1:
1469 if args[1] == '--':
1470 if len(args) > 2:
1471 extra_args = args[2:]
1472 else:
1473 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001474
maruel380e3262016-08-31 16:10:06 -07001475 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001476 request = net.url_read_json(url)
1477 if not request:
1478 print >> sys.stderr, 'Failed to retrieve request data for the task'
1479 return 1
1480
maruel12e30012015-10-09 11:55:35 -07001481 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001482 if fs.isdir(workdir):
1483 parser.error('Please delete the directory \'work\' first')
1484 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001485
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001486 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001487 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001488 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001489 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001490 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001491 for i in properties['env']:
1492 key = i['key'].encode('utf-8')
1493 if not i['value']:
1494 env.pop(key, None)
1495 else:
1496 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001497
nodir152cba62016-05-12 16:08:56 -07001498 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001499 # Create the tree.
1500 with isolateserver.get_storage(
1501 properties['inputs_ref']['isolatedserver'],
1502 properties['inputs_ref']['namespace']) as storage:
1503 bundle = isolateserver.fetch_isolated(
1504 properties['inputs_ref']['isolated'],
1505 storage,
1506 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001507 workdir,
1508 False)
maruel29ab2fd2015-10-16 11:44:01 -07001509 command = bundle.command
1510 if bundle.relative_cwd:
1511 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001512 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001513 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001514 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001515 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001516 if not options.output_dir and new_command != command:
1517 parser.error('The task has outputs, you must use --output-dir')
1518 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001519 else:
1520 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001521 try:
maruel18122c62015-10-23 06:31:23 -07001522 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001523 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001524 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001525 print >> sys.stderr, str(e)
1526 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001527
1528
maruel0eb1d1b2015-10-02 14:48:21 -07001529@subcommand.usage('bot_id')
1530def CMDterminate(parser, args):
1531 """Tells a bot to gracefully shut itself down as soon as it can.
1532
1533 This is done by completing whatever current task there is then exiting the bot
1534 process.
1535 """
1536 parser.add_option(
1537 '--wait', action='store_true', help='Wait for the bot to terminate')
1538 options, args = parser.parse_args(args)
1539 if len(args) != 1:
1540 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001541 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001542 request = net.url_read_json(url, data={})
1543 if not request:
1544 print >> sys.stderr, 'Failed to ask for termination'
1545 return 1
1546 if options.wait:
1547 return collect(
maruel9531ce02016-04-13 06:11:23 -07001548 options.swarming, [request['task_id']], 0., False, False, None, None,
1549 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001550 return 0
1551
1552
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001553@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001554def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001555 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001556
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001557 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001558 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001559
1560 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001561
1562 Passes all extra arguments provided after '--' as additional command line
1563 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001564 """
1565 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001566 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001567 parser.add_option(
1568 '--dump-json',
1569 metavar='FILE',
1570 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001571 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001572 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001573 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001574 tasks = trigger_task_shards(
1575 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001576 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001577 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001578 tasks_sorted = sorted(
1579 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001580 if options.dump_json:
1581 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001582 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001583 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001584 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001585 }
maruel46b015f2015-10-13 18:40:35 -07001586 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001587 print('To collect results, use:')
1588 print(' swarming.py collect -S %s --json %s' %
1589 (options.swarming, options.dump_json))
1590 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001591 print('To collect results, use:')
1592 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001593 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1594 print('Or visit:')
1595 for t in tasks_sorted:
1596 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001597 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001598 except Failure:
1599 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001600 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001601
1602
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001603class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001604 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001605 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001606 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001607 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001608 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001609 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001610 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001611 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001612 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001613 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001614
1615 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001616 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001617 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001618 auth.process_auth_options(self, options)
1619 user = self._process_swarming(options)
1620 if hasattr(options, 'user') and not options.user:
1621 options.user = user
1622 return options, args
1623
1624 def _process_swarming(self, options):
1625 """Processes the --swarming option and aborts if not specified.
1626
1627 Returns the identity as determined by the server.
1628 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001629 if not options.swarming:
1630 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001631 try:
1632 options.swarming = net.fix_url(options.swarming)
1633 except ValueError as e:
1634 self.error('--swarming %s' % e)
1635 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001636 try:
1637 user = auth.ensure_logged_in(options.swarming)
1638 except ValueError as e:
1639 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001640 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001641
1642
1643def main(args):
1644 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001645 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001646
1647
1648if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001649 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001650 fix_encoding.fix_encoding()
1651 tools.disable_buffering()
1652 colorama.init()
1653 sys.exit(main(sys.argv[1:]))