blob: 24743a5a632e6b1e5e7fa6d41280623b8d539864 [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
maruel11e31af2017-02-15 07:30:50 -08008__version__ = '0.8.10'
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
maruel11e31af2017-02-15 07:30:50 -080018import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040038import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
maruelc070e672016-02-22 17:32:57 -080040import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000041
42
tansella4949442016-06-23 22:34:32 -070043ROOT_DIR = os.path.dirname(os.path.abspath(
44 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050045
46
47class Failure(Exception):
48 """Generic failure."""
49 pass
50
51
52### Isolated file handling.
53
54
maruel77f720b2015-09-15 12:35:22 -070055def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050056 """Archives a .isolated file if needed.
57
58 Returns the file hash to trigger and a bool specifying if it was a file (True)
59 or a hash (False).
60 """
61 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070062 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070063 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050064 if not file_hash:
65 on_error.report('Archival failure %s' % arg)
66 return None, True
67 return file_hash, True
68 elif isolated_format.is_valid_hash(arg, algo):
69 return arg, False
70 else:
71 on_error.report('Invalid hash %s' % arg)
72 return None, False
73
74
75def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050076 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050077
78 Returns:
maruel77f720b2015-09-15 12:35:22 -070079 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050080 """
81 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070082 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050083 if not options.isolated:
84 if '--' in args:
85 index = args.index('--')
86 isolated_cmd_args = args[index+1:]
87 args = args[:index]
88 else:
89 # optparse eats '--' sometimes.
90 isolated_cmd_args = args[1:]
91 args = args[:1]
92 if len(args) != 1:
93 raise ValueError(
94 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
95 'process.')
96 # Old code. To be removed eventually.
97 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070098 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050099 if not options.isolated:
100 raise ValueError('Invalid argument %s' % args[0])
101 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500102 if '--' in args:
103 index = args.index('--')
104 isolated_cmd_args = args[index+1:]
105 if index != 0:
106 raise ValueError('Unexpected arguments.')
107 else:
108 # optparse eats '--' sometimes.
109 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500111 # If a file name was passed, use its base name of the isolated hash.
112 # Otherwise, use user name as an approximation of a task name.
113 if not options.task_name:
114 if is_file:
115 key = os.path.splitext(os.path.basename(args[0]))[0]
116 else:
117 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500118 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500119 key,
120 '_'.join(
121 '%s=%s' % (k, v)
122 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500123 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500124
maruel77f720b2015-09-15 12:35:22 -0700125 inputs_ref = FilesRef(
nodir152cba62016-05-12 16:08:56 -0700126 isolated=options.isolated,
127 isolatedserver=options.isolate_server,
128 namespace=options.namespace)
maruel77f720b2015-09-15 12:35:22 -0700129 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500130
131
132### Triggering.
133
134
maruel77f720b2015-09-15 12:35:22 -0700135# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -0700136CipdPackage = collections.namedtuple(
137 'CipdPackage',
138 [
139 'package_name',
140 'path',
141 'version',
142 ])
143
144
145# See ../appengine/swarming/swarming_rpcs.py.
146CipdInput = collections.namedtuple(
147 'CipdInput',
148 [
149 'client_package',
150 'packages',
151 'server',
152 ])
153
154
155# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700156FilesRef = collections.namedtuple(
157 'FilesRef',
158 [
159 'isolated',
160 'isolatedserver',
161 'namespace',
162 ])
163
164
165# See ../appengine/swarming/swarming_rpcs.py.
166TaskProperties = collections.namedtuple(
167 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500168 [
maruel681d6802017-01-17 16:56:03 -0800169 'caches',
borenet02f772b2016-06-22 12:42:19 -0700170 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500172 'dimensions',
173 'env',
maruel77f720b2015-09-15 12:35:22 -0700174 'execution_timeout_secs',
175 'extra_args',
176 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500177 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700178 'inputs_ref',
179 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700180 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700181 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700182 ])
183
184
185# See ../appengine/swarming/swarming_rpcs.py.
186NewTaskRequest = collections.namedtuple(
187 'NewTaskRequest',
188 [
189 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500190 'name',
maruel77f720b2015-09-15 12:35:22 -0700191 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500192 'priority',
maruel77f720b2015-09-15 12:35:22 -0700193 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700194 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500195 'tags',
196 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197 ])
198
199
maruel77f720b2015-09-15 12:35:22 -0700200def namedtuple_to_dict(value):
201 """Recursively converts a namedtuple to a dict."""
202 out = dict(value._asdict())
203 for k, v in out.iteritems():
204 if hasattr(v, '_asdict'):
205 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700206 elif isinstance(v, (list, tuple)):
207 l = []
208 for elem in v:
209 if hasattr(elem, '_asdict'):
210 l.append(namedtuple_to_dict(elem))
211 else:
212 l.append(elem)
213 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700214 return out
215
216
vadimsh93d167c2016-09-13 11:31:51 -0700217def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800218 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700219
220 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500221 """
maruel77f720b2015-09-15 12:35:22 -0700222 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700223 if hide_token:
224 if out['service_account_token'] not in (None, 'bot', 'none'):
225 out['service_account_token'] = '<hidden>'
226 # Don't send 'service_account_token' if it is None to avoid confusing older
227 # version of the server that doesn't know about 'service_account_token'.
228 if out['service_account_token'] in (None, 'none'):
229 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700230 # Maps are not supported until protobuf v3.
231 out['properties']['dimensions'] = [
232 {'key': k, 'value': v}
233 for k, v in out['properties']['dimensions'].iteritems()
234 ]
235 out['properties']['dimensions'].sort(key=lambda x: x['key'])
236 out['properties']['env'] = [
237 {'key': k, 'value': v}
238 for k, v in out['properties']['env'].iteritems()
239 ]
240 out['properties']['env'].sort(key=lambda x: x['key'])
241 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242
243
maruel77f720b2015-09-15 12:35:22 -0700244def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500245 """Triggers a request on the Swarming server and returns the json data.
246
247 It's the low-level function.
248
249 Returns:
250 {
251 'request': {
252 'created_ts': u'2010-01-02 03:04:05',
253 'name': ..
254 },
255 'task_id': '12300',
256 }
257 """
258 logging.info('Triggering: %s', raw_request['name'])
259
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500260 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700261 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500262 if not result:
263 on_error.report('Failed to trigger task %s' % raw_request['name'])
264 return None
maruele557bce2015-11-17 09:01:27 -0800265 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800266 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800267 msg = 'Failed to trigger task %s' % raw_request['name']
268 if result['error'].get('errors'):
269 for err in result['error']['errors']:
270 if err.get('message'):
271 msg += '\nMessage: %s' % err['message']
272 if err.get('debugInfo'):
273 msg += '\nDebug info:\n%s' % err['debugInfo']
274 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800275 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800276
277 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800278 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500279 return result
280
281
282def setup_googletest(env, shards, index):
283 """Sets googletest specific environment variables."""
284 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700285 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
286 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
287 env = env[:]
288 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
289 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500290 return env
291
292
293def trigger_task_shards(swarming, task_request, shards):
294 """Triggers one or many subtasks of a sharded task.
295
296 Returns:
297 Dict with task details, returned to caller as part of --dump-json output.
298 None in case of failure.
299 """
300 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700301 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500302 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700303 req['properties']['env'] = setup_googletest(
304 req['properties']['env'], shards, index)
305 req['name'] += ':%s:%s' % (index, shards)
306 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500307
308 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500309 tasks = {}
310 priority_warning = False
311 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700312 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500313 if not task:
314 break
315 logging.info('Request result: %s', task)
316 if (not priority_warning and
317 task['request']['priority'] != task_request.priority):
318 priority_warning = True
319 print >> sys.stderr, (
320 'Priority was reset to %s' % task['request']['priority'])
321 tasks[request['name']] = {
322 'shard_index': index,
323 'task_id': task['task_id'],
324 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
325 }
326
327 # Some shards weren't triggered. Abort everything.
328 if len(tasks) != len(requests):
329 if tasks:
330 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
331 len(tasks), len(requests))
332 for task_dict in tasks.itervalues():
333 abort_task(swarming, task_dict['task_id'])
334 return None
335
336 return tasks
337
338
vadimsh93d167c2016-09-13 11:31:51 -0700339def mint_service_account_token(service_account):
340 """Given a service account name returns a delegation token for this account.
341
342 The token is generated based on triggering user's credentials. It is passed
343 to Swarming, that uses it when running tasks.
344 """
345 logging.info(
346 'Generating delegation token for service account "%s"', service_account)
347 raise NotImplementedError('Custom service accounts are not implemented yet')
348
349
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500350### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000351
352
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700353# How often to print status updates to stdout in 'collect'.
354STATUS_UPDATE_INTERVAL = 15 * 60.
355
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400356
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400357class State(object):
358 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000359
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400360 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
361 values are part of the API so if they change, the API changed.
362
363 It's in fact an enum. Values should be in decreasing order of importance.
364 """
365 RUNNING = 0x10
366 PENDING = 0x20
367 EXPIRED = 0x30
368 TIMED_OUT = 0x40
369 BOT_DIED = 0x50
370 CANCELED = 0x60
371 COMPLETED = 0x70
372
maruel77f720b2015-09-15 12:35:22 -0700373 STATES = (
374 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
375 'COMPLETED')
376 STATES_RUNNING = ('RUNNING', 'PENDING')
377 STATES_NOT_RUNNING = (
378 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
379 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
380 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400381
382 _NAMES = {
383 RUNNING: 'Running',
384 PENDING: 'Pending',
385 EXPIRED: 'Expired',
386 TIMED_OUT: 'Execution timed out',
387 BOT_DIED: 'Bot died',
388 CANCELED: 'User canceled',
389 COMPLETED: 'Completed',
390 }
391
maruel77f720b2015-09-15 12:35:22 -0700392 _ENUMS = {
393 'RUNNING': RUNNING,
394 'PENDING': PENDING,
395 'EXPIRED': EXPIRED,
396 'TIMED_OUT': TIMED_OUT,
397 'BOT_DIED': BOT_DIED,
398 'CANCELED': CANCELED,
399 'COMPLETED': COMPLETED,
400 }
401
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400402 @classmethod
403 def to_string(cls, state):
404 """Returns a user-readable string representing a State."""
405 if state not in cls._NAMES:
406 raise ValueError('Invalid state %s' % state)
407 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000408
maruel77f720b2015-09-15 12:35:22 -0700409 @classmethod
410 def from_enum(cls, state):
411 """Returns int value based on the string."""
412 if state not in cls._ENUMS:
413 raise ValueError('Invalid state %s' % state)
414 return cls._ENUMS[state]
415
maruel@chromium.org0437a732013-08-27 16:05:52 +0000416
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700417class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700418 """Assembles task execution summary (for --task-summary-json output).
419
420 Optionally fetches task outputs from isolate server to local disk (used when
421 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700422
423 This object is shared among multiple threads running 'retrieve_results'
424 function, in particular they call 'process_shard_result' method in parallel.
425 """
426
maruel0eb1d1b2015-10-02 14:48:21 -0700427 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700428 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
429
430 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700431 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700432 shard_count: expected number of task shards.
433 """
maruel12e30012015-10-09 11:55:35 -0700434 self.task_output_dir = (
435 unicode(os.path.abspath(task_output_dir))
436 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700437 self.shard_count = shard_count
438
439 self._lock = threading.Lock()
440 self._per_shard_results = {}
441 self._storage = None
442
nodire5028a92016-04-29 14:38:21 -0700443 if self.task_output_dir:
444 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445
Vadim Shtayurab450c602014-05-12 19:23:25 -0700446 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447 """Stores results of a single task shard, fetches output files if necessary.
448
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400449 Modifies |result| in place.
450
maruel77f720b2015-09-15 12:35:22 -0700451 shard_index is 0-based.
452
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700453 Called concurrently from multiple threads.
454 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700455 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700456 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700457 if shard_index < 0 or shard_index >= self.shard_count:
458 logging.warning(
459 'Shard index %d is outside of expected range: [0; %d]',
460 shard_index, self.shard_count - 1)
461 return
462
maruel77f720b2015-09-15 12:35:22 -0700463 if result.get('outputs_ref'):
464 ref = result['outputs_ref']
465 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
466 ref['isolatedserver'],
467 urllib.urlencode(
468 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400469
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700470 # Store result dict of that shard, ignore results we've already seen.
471 with self._lock:
472 if shard_index in self._per_shard_results:
473 logging.warning('Ignoring duplicate shard index %d', shard_index)
474 return
475 self._per_shard_results[shard_index] = result
476
477 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700478 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400479 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700480 result['outputs_ref']['isolatedserver'],
481 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400482 if storage:
483 # Output files are supposed to be small and they are not reused across
484 # tasks. So use MemoryCache for them instead of on-disk cache. Make
485 # files writable, so that calling script can delete them.
486 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700487 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400488 storage,
489 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700490 os.path.join(self.task_output_dir, str(shard_index)),
491 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700492
493 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700494 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700495 with self._lock:
496 # Write an array of shard results with None for missing shards.
497 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700498 'shards': [
499 self._per_shard_results.get(i) for i in xrange(self.shard_count)
500 ],
501 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700502 # Write summary.json to task_output_dir as well.
503 if self.task_output_dir:
504 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700505 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700506 summary,
507 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700508 if self._storage:
509 self._storage.close()
510 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700511 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700512
513 def _get_storage(self, isolate_server, namespace):
514 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700515 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700516 with self._lock:
517 if not self._storage:
518 self._storage = isolateserver.get_storage(isolate_server, namespace)
519 else:
520 # Shards must all use exact same isolate server and namespace.
521 if self._storage.location != isolate_server:
522 logging.error(
523 'Task shards are using multiple isolate servers: %s and %s',
524 self._storage.location, isolate_server)
525 return None
526 if self._storage.namespace != namespace:
527 logging.error(
528 'Task shards are using multiple namespaces: %s and %s',
529 self._storage.namespace, namespace)
530 return None
531 return self._storage
532
533
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500534def now():
535 """Exists so it can be mocked easily."""
536 return time.time()
537
538
maruel77f720b2015-09-15 12:35:22 -0700539def parse_time(value):
540 """Converts serialized time from the API to datetime.datetime."""
541 # When microseconds are 0, the '.123456' suffix is elided. This means the
542 # serialized format is not consistent, which confuses the hell out of python.
543 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
544 try:
545 return datetime.datetime.strptime(value, fmt)
546 except ValueError:
547 pass
548 raise ValueError('Failed to parse %s' % value)
549
550
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700551def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700552 base_url, shard_index, task_id, timeout, should_stop, output_collector,
553 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400554 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700555
Vadim Shtayurab450c602014-05-12 19:23:25 -0700556 Returns:
557 <result dict> on success.
558 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700559 """
maruel71c61c82016-02-22 06:52:05 -0800560 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700561 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700562 if include_perf:
563 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700564 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700565 started = now()
566 deadline = started + timeout if timeout else None
567 attempt = 0
568
569 while not should_stop.is_set():
570 attempt += 1
571
572 # Waiting for too long -> give up.
573 current_time = now()
574 if deadline and current_time >= deadline:
575 logging.error('retrieve_results(%s) timed out on attempt %d',
576 base_url, attempt)
577 return None
578
579 # Do not spin too fast. Spin faster at the beginning though.
580 # Start with 1 sec delay and for each 30 sec of waiting add another second
581 # of delay, until hitting 15 sec ceiling.
582 if attempt > 1:
583 max_delay = min(15, 1 + (current_time - started) / 30.0)
584 delay = min(max_delay, deadline - current_time) if deadline else max_delay
585 if delay > 0:
586 logging.debug('Waiting %.1f sec before retrying', delay)
587 should_stop.wait(delay)
588 if should_stop.is_set():
589 return None
590
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400591 # Disable internal retries in net.url_read_json, since we are doing retries
592 # ourselves.
593 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700594 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
595 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400596 result = net.url_read_json(result_url, retry_50x=False)
597 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400598 continue
maruel77f720b2015-09-15 12:35:22 -0700599
maruelbf53e042015-12-01 15:00:51 -0800600 if result.get('error'):
601 # An error occurred.
602 if result['error'].get('errors'):
603 for err in result['error']['errors']:
604 logging.warning(
605 'Error while reading task: %s; %s',
606 err.get('message'), err.get('debugInfo'))
607 elif result['error'].get('message'):
608 logging.warning(
609 'Error while reading task: %s', result['error']['message'])
610 continue
611
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400612 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700613 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400614 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700615 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700616 # Record the result, try to fetch attached output files (if any).
617 if output_collector:
618 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700619 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700620 if result.get('internal_failure'):
621 logging.error('Internal error!')
622 elif result['state'] == 'BOT_DIED':
623 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700624 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625
626
maruel77f720b2015-09-15 12:35:22 -0700627def convert_to_old_format(result):
628 """Converts the task result data from Endpoints API format to old API format
629 for compatibility.
630
631 This goes into the file generated as --task-summary-json.
632 """
633 # Sets default.
634 result.setdefault('abandoned_ts', None)
635 result.setdefault('bot_id', None)
636 result.setdefault('bot_version', None)
637 result.setdefault('children_task_ids', [])
638 result.setdefault('completed_ts', None)
639 result.setdefault('cost_saved_usd', None)
640 result.setdefault('costs_usd', None)
641 result.setdefault('deduped_from', None)
642 result.setdefault('name', None)
643 result.setdefault('outputs_ref', None)
644 result.setdefault('properties_hash', None)
645 result.setdefault('server_versions', None)
646 result.setdefault('started_ts', None)
647 result.setdefault('tags', None)
648 result.setdefault('user', None)
649
650 # Convertion back to old API.
651 duration = result.pop('duration', None)
652 result['durations'] = [duration] if duration else []
653 exit_code = result.pop('exit_code', None)
654 result['exit_codes'] = [int(exit_code)] if exit_code else []
655 result['id'] = result.pop('task_id')
656 result['isolated_out'] = result.get('outputs_ref', None)
657 output = result.pop('output', None)
658 result['outputs'] = [output] if output else []
659 # properties_hash
660 # server_version
661 # Endpoints result 'state' as string. For compatibility with old code, convert
662 # to int.
663 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700664 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700665 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700666 if 'bot_dimensions' in result:
667 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700668 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700669 }
670 else:
671 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700672
673
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400675 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700676 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500677 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000678
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700679 Duplicate shards are ignored. Shards are yielded in order of completion.
680 Timed out shards are NOT yielded at all. Caller can compare number of yielded
681 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000682
683 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500684 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 +0000685 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500686
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700687 output_collector is an optional instance of TaskOutputCollector that will be
688 used to fetch files produced by a task from isolate server to the local disk.
689
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500690 Yields:
691 (index, result). In particular, 'result' is defined as the
692 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000693 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000694 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400695 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700696 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700697 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700698
maruel@chromium.org0437a732013-08-27 16:05:52 +0000699 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
700 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700701 # Adds a task to the thread pool to call 'retrieve_results' and return
702 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400703 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700704 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000705 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400706 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700707 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700708
709 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400710 for shard_index, task_id in enumerate(task_ids):
711 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700712
713 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400714 shards_remaining = range(len(task_ids))
715 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700716 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700717 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700718 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700719 shard_index, result = results_channel.pull(
720 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700721 except threading_utils.TaskChannel.Timeout:
722 if print_status_updates:
723 print(
724 'Waiting for results from the following shards: %s' %
725 ', '.join(map(str, shards_remaining)))
726 sys.stdout.flush()
727 continue
728 except Exception:
729 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700730
731 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700732 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000733 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500734 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000735 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700736
Vadim Shtayurab450c602014-05-12 19:23:25 -0700737 # Yield back results to the caller.
738 assert shard_index in shards_remaining
739 shards_remaining.remove(shard_index)
740 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700741
maruel@chromium.org0437a732013-08-27 16:05:52 +0000742 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700743 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000744 should_stop.set()
745
746
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400747def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000748 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700749 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400750 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700751 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
752 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400753 else:
754 pending = 'N/A'
755
maruel77f720b2015-09-15 12:35:22 -0700756 if metadata.get('duration') is not None:
757 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400758 else:
759 duration = 'N/A'
760
maruel77f720b2015-09-15 12:35:22 -0700761 if metadata.get('exit_code') is not None:
762 # Integers are encoded as string to not loose precision.
763 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400764 else:
765 exit_code = 'N/A'
766
767 bot_id = metadata.get('bot_id') or 'N/A'
768
maruel77f720b2015-09-15 12:35:22 -0700769 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400770 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400771 tag_footer = (
772 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
773 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400774
775 tag_len = max(len(tag_header), len(tag_footer))
776 dash_pad = '+-%s-+\n' % ('-' * tag_len)
777 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
778 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
779
780 header = dash_pad + tag_header + dash_pad
781 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700782 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400783 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000784
785
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700786def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700787 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700788 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700789 """Retrieves results of a Swarming task.
790
791 Returns:
792 process exit code that should be returned to the user.
793 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700794 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700795 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700796
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700797 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700798 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400799 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700800 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400801 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400802 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700803 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700804 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700805
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400806 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700807 shard_exit_code = metadata.get('exit_code')
808 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700809 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700810 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700811 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400812 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700813 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700814
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700815 if decorate:
leileied181762016-10-13 14:24:59 -0700816 s = decorate_shard_output(swarming, index, metadata).encode(
817 'utf-8', 'replace')
818 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400819 if len(seen_shards) < len(task_ids):
820 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700821 else:
maruel77f720b2015-09-15 12:35:22 -0700822 print('%s: %s %s' % (
823 metadata.get('bot_id', 'N/A'),
824 metadata['task_id'],
825 shard_exit_code))
826 if metadata['output']:
827 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400828 if output:
829 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700830 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700831 summary = output_collector.finalize()
832 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700833 # TODO(maruel): Make this optional.
834 for i in summary['shards']:
835 if i:
836 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700837 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700838
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400839 if decorate and total_duration:
840 print('Total duration: %.1fs' % total_duration)
841
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400842 if len(seen_shards) != len(task_ids):
843 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700844 print >> sys.stderr, ('Results from some shards are missing: %s' %
845 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700846 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700847
maruela5490782015-09-30 10:56:59 -0700848 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000849
850
maruel77f720b2015-09-15 12:35:22 -0700851### API management.
852
853
854class APIError(Exception):
855 pass
856
857
858def endpoints_api_discovery_apis(host):
859 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
860 the APIs exposed by a host.
861
862 https://developers.google.com/discovery/v1/reference/apis/list
863 """
maruel380e3262016-08-31 16:10:06 -0700864 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
865 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700866 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
867 if data is None:
868 raise APIError('Failed to discover APIs on %s' % host)
869 out = {}
870 for api in data['items']:
871 if api['id'] == 'discovery:v1':
872 continue
873 # URL is of the following form:
874 # url = host + (
875 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
876 api_data = net.url_read_json(api['discoveryRestUrl'])
877 if api_data is None:
878 raise APIError('Failed to discover %s on %s' % (api['id'], host))
879 out[api['id']] = api_data
880 return out
881
882
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500883### Commands.
884
885
886def abort_task(_swarming, _manifest):
887 """Given a task manifest that was triggered, aborts its execution."""
888 # TODO(vadimsh): No supported by the server yet.
889
890
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400891def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800892 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500893 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500894 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500895 dest='dimensions', metavar='FOO bar',
896 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500897 parser.add_option_group(parser.filter_group)
898
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400899
Vadim Shtayurab450c602014-05-12 19:23:25 -0700900def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400901 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700902 parser.sharding_group.add_option(
903 '--shards', type='int', default=1,
904 help='Number of shards to trigger and collect.')
905 parser.add_option_group(parser.sharding_group)
906
907
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400908def add_trigger_options(parser):
909 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500910 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400911 add_filter_options(parser)
912
maruel681d6802017-01-17 16:56:03 -0800913 group = optparse.OptionGroup(parser, 'Task properties')
914 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500915 '-s', '--isolated',
916 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800917 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500918 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700919 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800920 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400921 '--idempotent', action='store_true', default=False,
922 help='When set, the server will actively try to find a previous task '
923 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800924 group.add_option(
iannuccieee1bca2016-10-28 13:16:23 -0700925 '--secret-bytes-path',
iannuccidc80dfb2016-10-28 12:50:20 -0700926 help='The optional path to a file containing the secret_bytes to use with'
927 'this task.')
maruel681d6802017-01-17 16:56:03 -0800928 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400929 '--hard-timeout', type='int', default=60*60,
930 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800931 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400932 '--io-timeout', type='int', default=20*60,
933 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800934 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500935 '--raw-cmd', action='store_true', default=False,
936 help='When set, the command after -- is used as-is without run_isolated. '
937 'In this case, no .isolated file is expected.')
maruel681d6802017-01-17 16:56:03 -0800938 group.add_option(
borenet02f772b2016-06-22 12:42:19 -0700939 '--cipd-package', action='append', default=[],
940 help='CIPD packages to install on the Swarming bot. Uses the format: '
941 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800942 group.add_option(
943 '--named-cache', action='append', nargs=2, default=[],
944 help='"<name> <relpath>" items to keep a persistent bot managed cache')
945 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700946 '--service-account',
947 help='Name of a service account to run the task as. Only literal "bot" '
948 'string can be specified currently (to run the task under bot\'s '
949 'account). Don\'t use task service accounts if not given '
950 '(default).')
maruel681d6802017-01-17 16:56:03 -0800951 group.add_option(
aludwincc5524e2016-10-28 10:25:24 -0700952 '-o', '--output', action='append', default=[],
953 help='A list of files to return in addition to those written to'
954 '$(ISOLATED_OUTDIR). An error will occur if a file specified by'
955 'this option is also written directly to $(ISOLATED_OUTDIR).')
maruel681d6802017-01-17 16:56:03 -0800956 parser.add_option_group(group)
957
958 group = optparse.OptionGroup(parser, 'Task request')
959 group.add_option(
960 '--priority', type='int', default=100,
961 help='The lower value, the more important the task is')
962 group.add_option(
963 '-T', '--task-name',
964 help='Display name of the task. Defaults to '
965 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
966 'isolated file is provided, if a hash is provided, it defaults to '
967 '<user>/<dimensions>/<isolated hash>/<timestamp>')
968 group.add_option(
969 '--tags', action='append', default=[],
970 help='Tags to assign to the task.')
971 group.add_option(
972 '--user', default='',
973 help='User associated with the task. Defaults to authenticated user on '
974 'the server.')
975 group.add_option(
976 '--expiration', type='int', default=6*60*60,
977 help='Seconds to allow the task to be pending for a bot to run before '
978 'this task request expires.')
979 group.add_option(
980 '--deadline', type='int', dest='expiration',
981 help=optparse.SUPPRESS_HELP)
982 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000983
984
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500985def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700986 """Processes trigger options and does preparatory steps.
987
988 Uploads files to isolate server and generates service account tokens if
989 necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500990 """
991 options.dimensions = dict(options.dimensions)
992 options.env = dict(options.env)
993
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500994 if not options.dimensions:
995 parser.error('Please at least specify one --dimension')
996 if options.raw_cmd:
997 if not args:
998 parser.error(
999 'Arguments with --raw-cmd should be passed after -- as command '
1000 'delimiter.')
1001 if options.isolate_server:
1002 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
1003
1004 command = args
1005 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001006 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001007 options.user,
1008 '_'.join(
1009 '%s=%s' % (k, v)
1010 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -07001011 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001012 else:
nodir55be77b2016-05-03 09:39:57 -07001013 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001014 try:
maruel77f720b2015-09-15 12:35:22 -07001015 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001016 except ValueError as e:
1017 parser.error(str(e))
1018
borenet02f772b2016-06-22 12:42:19 -07001019 cipd_packages = []
1020 for p in options.cipd_package:
1021 split = p.split(':', 2)
1022 if len(split) != 3:
1023 parser.error('CIPD packages must take the form: path:package:version')
1024 cipd_packages.append(CipdPackage(
1025 package_name=split[1],
1026 path=split[0],
1027 version=split[2]))
1028 cipd_input = None
1029 if cipd_packages:
1030 cipd_input = CipdInput(
1031 client_package=None,
1032 packages=cipd_packages,
1033 server=None)
1034
iannuccidc80dfb2016-10-28 12:50:20 -07001035 secret_bytes = None
1036 if options.secret_bytes_path:
1037 with open(options.secret_bytes_path, 'r') as f:
1038 secret_bytes = f.read().encode('base64')
1039
maruel681d6802017-01-17 16:56:03 -08001040 caches = [
1041 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1042 for i in options.named_cache
1043 ]
nodir152cba62016-05-12 16:08:56 -07001044 # If inputs_ref.isolated is used, command is actually extra_args.
1045 # Otherwise it's an actual command to run.
1046 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -07001047 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001048 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001049 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -07001050 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001051 dimensions=options.dimensions,
1052 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001053 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -07001054 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -07001055 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001056 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001057 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001058 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001059 outputs=options.output,
1060 secret_bytes=secret_bytes)
maruel8fce7962015-10-21 11:17:47 -07001061 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1062 parser.error('--tags must be in the format key:value')
vadimsh93d167c2016-09-13 11:31:51 -07001063
1064 # Convert a service account email to a signed service account token to pass
1065 # to Swarming.
1066 service_account_token = None
1067 if options.service_account in ('bot', 'none'):
1068 service_account_token = options.service_account
1069 elif options.service_account:
1070 # pylint: disable=assignment-from-no-return
1071 service_account_token = mint_service_account_token(options.service_account)
1072
maruel77f720b2015-09-15 12:35:22 -07001073 return NewTaskRequest(
1074 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001075 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001076 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001077 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001078 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001079 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001080 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001081 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001082
1083
1084def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001085 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001086 '-t', '--timeout', type='float',
1087 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1088 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001089 parser.group_logging.add_option(
1090 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001091 parser.group_logging.add_option(
1092 '--print-status-updates', action='store_true',
1093 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001094 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001095 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001096 '--task-summary-json',
1097 metavar='FILE',
1098 help='Dump a summary of task results to this file as json. It contains '
1099 'only shards statuses as know to server directly. Any output files '
1100 'emitted by the task can be collected by using --task-output-dir')
1101 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001102 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001103 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001104 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001105 'directory contains per-shard directory with output files produced '
1106 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001107 parser.task_output_group.add_option(
1108 '--perf', action='store_true', default=False,
1109 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001110 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001111
1112
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001113@subcommand.usage('bots...')
1114def CMDbot_delete(parser, args):
1115 """Forcibly deletes bots from the Swarming server."""
1116 parser.add_option(
1117 '-f', '--force', action='store_true',
1118 help='Do not prompt for confirmation')
1119 options, args = parser.parse_args(args)
1120 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001121 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001122
1123 bots = sorted(args)
1124 if not options.force:
1125 print('Delete the following bots?')
1126 for bot in bots:
1127 print(' %s' % bot)
1128 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1129 print('Goodbye.')
1130 return 1
1131
1132 result = 0
1133 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001134 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001135 if net.url_read_json(url, data={}, method='POST') is None:
1136 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001137 result = 1
1138 return result
1139
1140
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001141def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001142 """Returns information about the bots connected to the Swarming server."""
1143 add_filter_options(parser)
1144 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001145 '--dead-only', action='store_true',
1146 help='Only print dead bots, useful to reap them and reimage broken bots')
1147 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001148 '-k', '--keep-dead', action='store_true',
1149 help='Do not filter out dead bots')
1150 parser.filter_group.add_option(
1151 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001152 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001153 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001154
1155 if options.keep_dead and options.dead_only:
1156 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001157
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001158 bots = []
1159 cursor = None
1160 limit = 250
1161 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001162 base_url = (
maruel380e3262016-08-31 16:10:06 -07001163 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001164 while True:
1165 url = base_url
1166 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001167 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001168 data = net.url_read_json(url)
1169 if data is None:
1170 print >> sys.stderr, 'Failed to access %s' % options.swarming
1171 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001172 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001173 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001174 if not cursor:
1175 break
1176
maruel77f720b2015-09-15 12:35:22 -07001177 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001178 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001179 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001180 continue
maruel77f720b2015-09-15 12:35:22 -07001181 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001182 continue
1183
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001184 # If the user requested to filter on dimensions, ensure the bot has all the
1185 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001186 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001187 for key, value in options.dimensions:
1188 if key not in dimensions:
1189 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001190 # A bot can have multiple value for a key, for example,
1191 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1192 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001193 if isinstance(dimensions[key], list):
1194 if value not in dimensions[key]:
1195 break
1196 else:
1197 if value != dimensions[key]:
1198 break
1199 else:
maruel77f720b2015-09-15 12:35:22 -07001200 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001201 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001202 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001203 if bot.get('task_id'):
1204 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001205 return 0
1206
1207
maruelfd0a90c2016-06-10 11:51:10 -07001208@subcommand.usage('task_id')
1209def CMDcancel(parser, args):
1210 """Cancels a task."""
1211 options, args = parser.parse_args(args)
1212 if not args:
1213 parser.error('Please specify the task to cancel')
1214 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001215 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001216 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1217 print('Deleting %s failed. Probably already gone' % task_id)
1218 return 1
1219 return 0
1220
1221
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001222@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001223def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001224 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001225
1226 The result can be in multiple part if the execution was sharded. It can
1227 potentially have retries.
1228 """
1229 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001230 parser.add_option(
1231 '-j', '--json',
1232 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001233 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001234 if not args and not options.json:
1235 parser.error('Must specify at least one task id or --json.')
1236 if args and options.json:
1237 parser.error('Only use one of task id or --json.')
1238
1239 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001240 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001241 try:
maruel1ceb3872015-10-14 06:10:44 -07001242 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001243 data = json.load(f)
1244 except (IOError, ValueError):
1245 parser.error('Failed to open %s' % options.json)
1246 try:
1247 tasks = sorted(
1248 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1249 args = [t['task_id'] for t in tasks]
1250 except (KeyError, TypeError):
1251 parser.error('Failed to process %s' % options.json)
1252 if options.timeout is None:
1253 options.timeout = (
1254 data['request']['properties']['execution_timeout_secs'] +
1255 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001256 else:
1257 valid = frozenset('0123456789abcdef')
1258 if any(not valid.issuperset(task_id) for task_id in args):
1259 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001260
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001261 try:
1262 return collect(
1263 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001264 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001265 options.timeout,
1266 options.decorate,
1267 options.print_status_updates,
1268 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001269 options.task_output_dir,
1270 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001271 except Failure:
1272 on_error.report(None)
1273 return 1
1274
1275
maruelbea00862015-09-18 09:55:36 -07001276@subcommand.usage('[filename]')
1277def CMDput_bootstrap(parser, args):
1278 """Uploads a new version of bootstrap.py."""
1279 options, args = parser.parse_args(args)
1280 if len(args) != 1:
1281 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001282 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001283 path = unicode(os.path.abspath(args[0]))
1284 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001285 content = f.read().decode('utf-8')
1286 data = net.url_read_json(url, data={'content': content})
1287 print data
1288 return 0
1289
1290
1291@subcommand.usage('[filename]')
1292def CMDput_bot_config(parser, args):
1293 """Uploads a new version of bot_config.py."""
1294 options, args = parser.parse_args(args)
1295 if len(args) != 1:
1296 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001297 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001298 path = unicode(os.path.abspath(args[0]))
1299 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001300 content = f.read().decode('utf-8')
1301 data = net.url_read_json(url, data={'content': content})
1302 print data
1303 return 0
1304
1305
maruel77f720b2015-09-15 12:35:22 -07001306@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001307def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001308 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1309 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001310
1311 Examples:
maruel77f720b2015-09-15 12:35:22 -07001312 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001313 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001314
maruel77f720b2015-09-15 12:35:22 -07001315 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001316 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1317
1318 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1319 quoting is important!:
1320 swarming.py query -S server-url.com --limit 10 \\
1321 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001322 """
1323 CHUNK_SIZE = 250
1324
1325 parser.add_option(
1326 '-L', '--limit', type='int', default=200,
1327 help='Limit to enforce on limitless items (like number of tasks); '
1328 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001329 parser.add_option(
1330 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001331 parser.add_option(
1332 '--progress', action='store_true',
1333 help='Prints a dot at each request to show progress')
1334 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001335 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001336 parser.error(
1337 'Must specify only method name and optionally query args properly '
1338 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001339 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001340 url = base_url
1341 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001342 # Check check, change if not working out.
1343 merge_char = '&' if '?' in url else '?'
1344 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001345 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001346 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001347 # TODO(maruel): Do basic diagnostic.
1348 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001349 return 1
1350
1351 # Some items support cursors. Try to get automatically if cursors are needed
1352 # by looking at the 'cursor' items.
1353 while (
1354 data.get('cursor') and
1355 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001356 merge_char = '&' if '?' in base_url else '?'
1357 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001358 if options.limit:
1359 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001360 if options.progress:
1361 sys.stdout.write('.')
1362 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001363 new = net.url_read_json(url)
1364 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001365 if options.progress:
1366 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001367 print >> sys.stderr, 'Failed to access %s' % options.swarming
1368 return 1
maruel81b37132015-10-21 06:42:13 -07001369 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001370 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001371
maruel77f720b2015-09-15 12:35:22 -07001372 if options.progress:
1373 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001374 if options.limit and len(data.get('items', [])) > options.limit:
1375 data['items'] = data['items'][:options.limit]
1376 data.pop('cursor', None)
1377
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001378 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001379 options.json = unicode(os.path.abspath(options.json))
1380 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001381 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001382 try:
maruel77f720b2015-09-15 12:35:22 -07001383 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001384 sys.stdout.write('\n')
1385 except IOError:
1386 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001387 return 0
1388
1389
maruel77f720b2015-09-15 12:35:22 -07001390def CMDquery_list(parser, args):
1391 """Returns list of all the Swarming APIs that can be used with command
1392 'query'.
1393 """
1394 parser.add_option(
1395 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1396 options, args = parser.parse_args(args)
1397 if args:
1398 parser.error('No argument allowed.')
1399
1400 try:
1401 apis = endpoints_api_discovery_apis(options.swarming)
1402 except APIError as e:
1403 parser.error(str(e))
1404 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001405 options.json = unicode(os.path.abspath(options.json))
1406 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001407 json.dump(apis, f)
1408 else:
1409 help_url = (
1410 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1411 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001412 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1413 if i:
1414 print('')
maruel77f720b2015-09-15 12:35:22 -07001415 print api_id
maruel11e31af2017-02-15 07:30:50 -08001416 print ' ' + api['description'].strip()
1417 if 'resources' in api:
1418 # Old.
1419 for j, (resource_name, resource) in enumerate(
1420 sorted(api['resources'].iteritems())):
1421 if j:
1422 print('')
1423 for method_name, method in sorted(resource['methods'].iteritems()):
1424 # Only list the GET ones.
1425 if method['httpMethod'] != 'GET':
1426 continue
1427 print '- %s.%s: %s' % (
1428 resource_name, method_name, method['path'])
1429 print('\n'.join(
1430 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1431 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1432 else:
1433 # New.
1434 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001435 # Only list the GET ones.
1436 if method['httpMethod'] != 'GET':
1437 continue
maruel11e31af2017-02-15 07:30:50 -08001438 print '- %s: %s' % (method['id'], method['path'])
1439 print('\n'.join(
1440 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001441 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1442 return 0
1443
1444
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001445@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001446def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001447 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001448
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001449 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001450 """
1451 add_trigger_options(parser)
1452 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001453 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001454 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001455 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001456 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001457 tasks = trigger_task_shards(
1458 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001459 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001460 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001461 'Failed to trigger %s(%s): %s' %
1462 (options.task_name, args[0], e.args[0]))
1463 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001464 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001465 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001466 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001467 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001468 task_ids = [
1469 t['task_id']
1470 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1471 ]
maruel71c61c82016-02-22 06:52:05 -08001472 if options.timeout is None:
1473 options.timeout = (
1474 task_request.properties.execution_timeout_secs +
1475 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001476 try:
1477 return collect(
1478 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001479 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001480 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001481 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001482 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001483 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001484 options.task_output_dir,
1485 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001486 except Failure:
1487 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001488 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001489
1490
maruel18122c62015-10-23 06:31:23 -07001491@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001492def CMDreproduce(parser, args):
1493 """Runs a task locally that was triggered on the server.
1494
1495 This running locally the same commands that have been run on the bot. The data
1496 downloaded will be in a subdirectory named 'work' of the current working
1497 directory.
maruel18122c62015-10-23 06:31:23 -07001498
1499 You can pass further additional arguments to the target command by passing
1500 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001501 """
maruelc070e672016-02-22 17:32:57 -08001502 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001503 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001504 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001505 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001506 extra_args = []
1507 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001508 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001509 if len(args) > 1:
1510 if args[1] == '--':
1511 if len(args) > 2:
1512 extra_args = args[2:]
1513 else:
1514 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001515
maruel380e3262016-08-31 16:10:06 -07001516 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001517 request = net.url_read_json(url)
1518 if not request:
1519 print >> sys.stderr, 'Failed to retrieve request data for the task'
1520 return 1
1521
maruel12e30012015-10-09 11:55:35 -07001522 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001523 if fs.isdir(workdir):
1524 parser.error('Please delete the directory \'work\' first')
1525 fs.mkdir(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001526
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001527 properties = request['properties']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001528 env = None
maruel29ab2fd2015-10-16 11:44:01 -07001529 if properties.get('env'):
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001530 env = os.environ.copy()
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001531 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001532 for i in properties['env']:
1533 key = i['key'].encode('utf-8')
1534 if not i['value']:
1535 env.pop(key, None)
1536 else:
1537 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001538
nodir152cba62016-05-12 16:08:56 -07001539 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001540 # Create the tree.
1541 with isolateserver.get_storage(
1542 properties['inputs_ref']['isolatedserver'],
1543 properties['inputs_ref']['namespace']) as storage:
1544 bundle = isolateserver.fetch_isolated(
1545 properties['inputs_ref']['isolated'],
1546 storage,
1547 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001548 workdir,
1549 False)
maruel29ab2fd2015-10-16 11:44:01 -07001550 command = bundle.command
1551 if bundle.relative_cwd:
1552 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001553 command.extend(properties.get('extra_args') or [])
maruelc070e672016-02-22 17:32:57 -08001554 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
nodirbe642ff2016-06-09 15:51:51 -07001555 new_command = run_isolated.process_command(
nodir90bc8dc2016-06-15 13:35:21 -07001556 command, options.output_dir, None)
maruelc070e672016-02-22 17:32:57 -08001557 if not options.output_dir and new_command != command:
1558 parser.error('The task has outputs, you must use --output-dir')
1559 command = new_command
maruel29ab2fd2015-10-16 11:44:01 -07001560 else:
1561 command = properties['command']
maruel77f720b2015-09-15 12:35:22 -07001562 try:
maruel18122c62015-10-23 06:31:23 -07001563 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001564 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001565 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001566 print >> sys.stderr, str(e)
1567 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001568
1569
maruel0eb1d1b2015-10-02 14:48:21 -07001570@subcommand.usage('bot_id')
1571def CMDterminate(parser, args):
1572 """Tells a bot to gracefully shut itself down as soon as it can.
1573
1574 This is done by completing whatever current task there is then exiting the bot
1575 process.
1576 """
1577 parser.add_option(
1578 '--wait', action='store_true', help='Wait for the bot to terminate')
1579 options, args = parser.parse_args(args)
1580 if len(args) != 1:
1581 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001582 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001583 request = net.url_read_json(url, data={})
1584 if not request:
1585 print >> sys.stderr, 'Failed to ask for termination'
1586 return 1
1587 if options.wait:
1588 return collect(
maruel9531ce02016-04-13 06:11:23 -07001589 options.swarming, [request['task_id']], 0., False, False, None, None,
1590 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001591 return 0
1592
1593
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001594@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001595def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001596 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001597
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001598 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001599 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001600
1601 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001602
1603 Passes all extra arguments provided after '--' as additional command line
1604 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001605 """
1606 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001607 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001608 parser.add_option(
1609 '--dump-json',
1610 metavar='FILE',
1611 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001612 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001613 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001614 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001615 tasks = trigger_task_shards(
1616 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001617 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001618 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001619 tasks_sorted = sorted(
1620 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001621 if options.dump_json:
1622 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001623 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001624 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001625 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001626 }
maruel46b015f2015-10-13 18:40:35 -07001627 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001628 print('To collect results, use:')
1629 print(' swarming.py collect -S %s --json %s' %
1630 (options.swarming, options.dump_json))
1631 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001632 print('To collect results, use:')
1633 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001634 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1635 print('Or visit:')
1636 for t in tasks_sorted:
1637 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001638 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001639 except Failure:
1640 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001641 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001642
1643
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001644class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001645 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001646 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001647 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001648 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001649 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001650 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001651 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001652 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001653 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001654 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001655
1656 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001657 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001658 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001659 auth.process_auth_options(self, options)
1660 user = self._process_swarming(options)
1661 if hasattr(options, 'user') and not options.user:
1662 options.user = user
1663 return options, args
1664
1665 def _process_swarming(self, options):
1666 """Processes the --swarming option and aborts if not specified.
1667
1668 Returns the identity as determined by the server.
1669 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001670 if not options.swarming:
1671 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001672 try:
1673 options.swarming = net.fix_url(options.swarming)
1674 except ValueError as e:
1675 self.error('--swarming %s' % e)
1676 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001677 try:
1678 user = auth.ensure_logged_in(options.swarming)
1679 except ValueError as e:
1680 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001681 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001682
1683
1684def main(args):
1685 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001686 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001687
1688
1689if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001690 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001691 fix_encoding.fix_encoding()
1692 tools.disable_buffering()
1693 colorama.init()
1694 sys.exit(main(sys.argv[1:]))