blob: f360b0a8a3939826cf40701ec68253be6a9a73e0 [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
iannucci31ab9192017-05-02 19:11:56 -070038import cipd
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040039import isolated_format
maruel@chromium.org7b844a62013-09-17 13:04:59 +000040import isolateserver
maruelc070e672016-02-22 17:32:57 -080041import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000042
43
tansella4949442016-06-23 22:34:32 -070044ROOT_DIR = os.path.dirname(os.path.abspath(
45 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050046
47
48class Failure(Exception):
49 """Generic failure."""
50 pass
51
52
53### Isolated file handling.
54
55
maruel77f720b2015-09-15 12:35:22 -070056def isolated_to_hash(arg, algo):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050057 """Archives a .isolated file if needed.
58
59 Returns the file hash to trigger and a bool specifying if it was a file (True)
60 or a hash (False).
61 """
62 if arg.endswith('.isolated'):
maruele7dc3872015-10-16 11:51:40 -070063 arg = unicode(os.path.abspath(arg))
maruel77f720b2015-09-15 12:35:22 -070064 file_hash = isolated_format.hash_file(arg, algo)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050065 if not file_hash:
66 on_error.report('Archival failure %s' % arg)
67 return None, True
68 return file_hash, True
69 elif isolated_format.is_valid_hash(arg, algo):
70 return arg, False
71 else:
72 on_error.report('Invalid hash %s' % arg)
73 return None, False
74
75
76def isolated_handle_options(options, args):
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050077 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050078
79 Returns:
maruel77f720b2015-09-15 12:35:22 -070080 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050081 """
82 isolated_cmd_args = []
maruel8fce7962015-10-21 11:17:47 -070083 is_file = False
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050084 if not options.isolated:
85 if '--' in args:
86 index = args.index('--')
87 isolated_cmd_args = args[index+1:]
88 args = args[:index]
89 else:
90 # optparse eats '--' sometimes.
91 isolated_cmd_args = args[1:]
92 args = args[:1]
93 if len(args) != 1:
94 raise ValueError(
95 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
96 'process.')
97 # Old code. To be removed eventually.
98 options.isolated, is_file = isolated_to_hash(
maruel77f720b2015-09-15 12:35:22 -070099 args[0], isolated_format.get_hash_algo(options.namespace))
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500100 if not options.isolated:
101 raise ValueError('Invalid argument %s' % args[0])
102 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500103 if '--' in args:
104 index = args.index('--')
105 isolated_cmd_args = args[index+1:]
106 if index != 0:
107 raise ValueError('Unexpected arguments.')
108 else:
109 # optparse eats '--' sometimes.
110 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500111
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500112 # If a file name was passed, use its base name of the isolated hash.
113 # Otherwise, use user name as an approximation of a task name.
114 if not options.task_name:
115 if is_file:
116 key = os.path.splitext(os.path.basename(args[0]))[0]
117 else:
118 key = options.user
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500119 options.task_name = u'%s/%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500120 key,
121 '_'.join(
122 '%s=%s' % (k, v)
123 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500124 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500125
maruel77f720b2015-09-15 12:35:22 -0700126 inputs_ref = FilesRef(
nodir152cba62016-05-12 16:08:56 -0700127 isolated=options.isolated,
128 isolatedserver=options.isolate_server,
129 namespace=options.namespace)
maruel77f720b2015-09-15 12:35:22 -0700130 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500131
132
133### Triggering.
134
135
maruel77f720b2015-09-15 12:35:22 -0700136# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -0700137CipdPackage = collections.namedtuple(
138 'CipdPackage',
139 [
140 'package_name',
141 'path',
142 'version',
143 ])
144
145
146# See ../appengine/swarming/swarming_rpcs.py.
147CipdInput = collections.namedtuple(
148 'CipdInput',
149 [
150 'client_package',
151 'packages',
152 'server',
153 ])
154
155
156# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700157FilesRef = collections.namedtuple(
158 'FilesRef',
159 [
160 'isolated',
161 'isolatedserver',
162 'namespace',
163 ])
164
165
166# See ../appengine/swarming/swarming_rpcs.py.
167TaskProperties = collections.namedtuple(
168 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500169 [
maruel681d6802017-01-17 16:56:03 -0800170 'caches',
borenet02f772b2016-06-22 12:42:19 -0700171 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500172 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500173 'dimensions',
174 'env',
maruel77f720b2015-09-15 12:35:22 -0700175 'execution_timeout_secs',
176 'extra_args',
177 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500178 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700179 'inputs_ref',
180 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700181 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700182 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700183 ])
184
185
186# See ../appengine/swarming/swarming_rpcs.py.
187NewTaskRequest = collections.namedtuple(
188 'NewTaskRequest',
189 [
190 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500191 'name',
maruel77f720b2015-09-15 12:35:22 -0700192 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500193 'priority',
maruel77f720b2015-09-15 12:35:22 -0700194 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700195 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500196 'tags',
197 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500198 ])
199
200
maruel77f720b2015-09-15 12:35:22 -0700201def namedtuple_to_dict(value):
202 """Recursively converts a namedtuple to a dict."""
203 out = dict(value._asdict())
204 for k, v in out.iteritems():
205 if hasattr(v, '_asdict'):
206 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700207 elif isinstance(v, (list, tuple)):
208 l = []
209 for elem in v:
210 if hasattr(elem, '_asdict'):
211 l.append(namedtuple_to_dict(elem))
212 else:
213 l.append(elem)
214 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700215 return out
216
217
vadimsh93d167c2016-09-13 11:31:51 -0700218def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800219 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700220
221 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500222 """
maruel77f720b2015-09-15 12:35:22 -0700223 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700224 if hide_token:
225 if out['service_account_token'] not in (None, 'bot', 'none'):
226 out['service_account_token'] = '<hidden>'
227 # Don't send 'service_account_token' if it is None to avoid confusing older
228 # version of the server that doesn't know about 'service_account_token'.
229 if out['service_account_token'] in (None, 'none'):
230 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700231 # Maps are not supported until protobuf v3.
232 out['properties']['dimensions'] = [
233 {'key': k, 'value': v}
234 for k, v in out['properties']['dimensions'].iteritems()
235 ]
236 out['properties']['dimensions'].sort(key=lambda x: x['key'])
237 out['properties']['env'] = [
238 {'key': k, 'value': v}
239 for k, v in out['properties']['env'].iteritems()
240 ]
241 out['properties']['env'].sort(key=lambda x: x['key'])
242 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500243
244
maruel77f720b2015-09-15 12:35:22 -0700245def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500246 """Triggers a request on the Swarming server and returns the json data.
247
248 It's the low-level function.
249
250 Returns:
251 {
252 'request': {
253 'created_ts': u'2010-01-02 03:04:05',
254 'name': ..
255 },
256 'task_id': '12300',
257 }
258 """
259 logging.info('Triggering: %s', raw_request['name'])
260
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500261 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700262 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500263 if not result:
264 on_error.report('Failed to trigger task %s' % raw_request['name'])
265 return None
maruele557bce2015-11-17 09:01:27 -0800266 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800267 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800268 msg = 'Failed to trigger task %s' % raw_request['name']
269 if result['error'].get('errors'):
270 for err in result['error']['errors']:
271 if err.get('message'):
272 msg += '\nMessage: %s' % err['message']
273 if err.get('debugInfo'):
274 msg += '\nDebug info:\n%s' % err['debugInfo']
275 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800276 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800277
278 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800279 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500280 return result
281
282
283def setup_googletest(env, shards, index):
284 """Sets googletest specific environment variables."""
285 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700286 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
287 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
288 env = env[:]
289 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
290 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500291 return env
292
293
294def trigger_task_shards(swarming, task_request, shards):
295 """Triggers one or many subtasks of a sharded task.
296
297 Returns:
298 Dict with task details, returned to caller as part of --dump-json output.
299 None in case of failure.
300 """
301 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700302 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500303 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700304 req['properties']['env'] = setup_googletest(
305 req['properties']['env'], shards, index)
306 req['name'] += ':%s:%s' % (index, shards)
307 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500308
309 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500310 tasks = {}
311 priority_warning = False
312 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700313 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500314 if not task:
315 break
316 logging.info('Request result: %s', task)
317 if (not priority_warning and
318 task['request']['priority'] != task_request.priority):
319 priority_warning = True
320 print >> sys.stderr, (
321 'Priority was reset to %s' % task['request']['priority'])
322 tasks[request['name']] = {
323 'shard_index': index,
324 'task_id': task['task_id'],
325 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
326 }
327
328 # Some shards weren't triggered. Abort everything.
329 if len(tasks) != len(requests):
330 if tasks:
331 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
332 len(tasks), len(requests))
333 for task_dict in tasks.itervalues():
334 abort_task(swarming, task_dict['task_id'])
335 return None
336
337 return tasks
338
339
vadimsh93d167c2016-09-13 11:31:51 -0700340def mint_service_account_token(service_account):
341 """Given a service account name returns a delegation token for this account.
342
343 The token is generated based on triggering user's credentials. It is passed
344 to Swarming, that uses it when running tasks.
345 """
346 logging.info(
347 'Generating delegation token for service account "%s"', service_account)
348 raise NotImplementedError('Custom service accounts are not implemented yet')
349
350
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500351### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000352
353
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700354# How often to print status updates to stdout in 'collect'.
355STATUS_UPDATE_INTERVAL = 15 * 60.
356
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400357
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400358class State(object):
359 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000360
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400361 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
362 values are part of the API so if they change, the API changed.
363
364 It's in fact an enum. Values should be in decreasing order of importance.
365 """
366 RUNNING = 0x10
367 PENDING = 0x20
368 EXPIRED = 0x30
369 TIMED_OUT = 0x40
370 BOT_DIED = 0x50
371 CANCELED = 0x60
372 COMPLETED = 0x70
373
maruel77f720b2015-09-15 12:35:22 -0700374 STATES = (
375 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
376 'COMPLETED')
377 STATES_RUNNING = ('RUNNING', 'PENDING')
378 STATES_NOT_RUNNING = (
379 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
380 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
381 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400382
383 _NAMES = {
384 RUNNING: 'Running',
385 PENDING: 'Pending',
386 EXPIRED: 'Expired',
387 TIMED_OUT: 'Execution timed out',
388 BOT_DIED: 'Bot died',
389 CANCELED: 'User canceled',
390 COMPLETED: 'Completed',
391 }
392
maruel77f720b2015-09-15 12:35:22 -0700393 _ENUMS = {
394 'RUNNING': RUNNING,
395 'PENDING': PENDING,
396 'EXPIRED': EXPIRED,
397 'TIMED_OUT': TIMED_OUT,
398 'BOT_DIED': BOT_DIED,
399 'CANCELED': CANCELED,
400 'COMPLETED': COMPLETED,
401 }
402
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400403 @classmethod
404 def to_string(cls, state):
405 """Returns a user-readable string representing a State."""
406 if state not in cls._NAMES:
407 raise ValueError('Invalid state %s' % state)
408 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000409
maruel77f720b2015-09-15 12:35:22 -0700410 @classmethod
411 def from_enum(cls, state):
412 """Returns int value based on the string."""
413 if state not in cls._ENUMS:
414 raise ValueError('Invalid state %s' % state)
415 return cls._ENUMS[state]
416
maruel@chromium.org0437a732013-08-27 16:05:52 +0000417
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700418class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700419 """Assembles task execution summary (for --task-summary-json output).
420
421 Optionally fetches task outputs from isolate server to local disk (used when
422 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700423
424 This object is shared among multiple threads running 'retrieve_results'
425 function, in particular they call 'process_shard_result' method in parallel.
426 """
427
maruel0eb1d1b2015-10-02 14:48:21 -0700428 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700429 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
430
431 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700432 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 shard_count: expected number of task shards.
434 """
maruel12e30012015-10-09 11:55:35 -0700435 self.task_output_dir = (
436 unicode(os.path.abspath(task_output_dir))
437 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438 self.shard_count = shard_count
439
440 self._lock = threading.Lock()
441 self._per_shard_results = {}
442 self._storage = None
443
nodire5028a92016-04-29 14:38:21 -0700444 if self.task_output_dir:
445 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700446
Vadim Shtayurab450c602014-05-12 19:23:25 -0700447 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 """Stores results of a single task shard, fetches output files if necessary.
449
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400450 Modifies |result| in place.
451
maruel77f720b2015-09-15 12:35:22 -0700452 shard_index is 0-based.
453
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 Called concurrently from multiple threads.
455 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700457 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700458 if shard_index < 0 or shard_index >= self.shard_count:
459 logging.warning(
460 'Shard index %d is outside of expected range: [0; %d]',
461 shard_index, self.shard_count - 1)
462 return
463
maruel77f720b2015-09-15 12:35:22 -0700464 if result.get('outputs_ref'):
465 ref = result['outputs_ref']
466 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
467 ref['isolatedserver'],
468 urllib.urlencode(
469 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400470
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700471 # Store result dict of that shard, ignore results we've already seen.
472 with self._lock:
473 if shard_index in self._per_shard_results:
474 logging.warning('Ignoring duplicate shard index %d', shard_index)
475 return
476 self._per_shard_results[shard_index] = result
477
478 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700479 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400480 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700481 result['outputs_ref']['isolatedserver'],
482 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400483 if storage:
484 # Output files are supposed to be small and they are not reused across
485 # tasks. So use MemoryCache for them instead of on-disk cache. Make
486 # files writable, so that calling script can delete them.
487 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700488 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400489 storage,
490 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700491 os.path.join(self.task_output_dir, str(shard_index)),
492 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700493
494 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700495 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700496 with self._lock:
497 # Write an array of shard results with None for missing shards.
498 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700499 'shards': [
500 self._per_shard_results.get(i) for i in xrange(self.shard_count)
501 ],
502 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700503 # Write summary.json to task_output_dir as well.
504 if self.task_output_dir:
505 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700506 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700507 summary,
508 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700509 if self._storage:
510 self._storage.close()
511 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700512 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700513
514 def _get_storage(self, isolate_server, namespace):
515 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700516 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700517 with self._lock:
518 if not self._storage:
519 self._storage = isolateserver.get_storage(isolate_server, namespace)
520 else:
521 # Shards must all use exact same isolate server and namespace.
522 if self._storage.location != isolate_server:
523 logging.error(
524 'Task shards are using multiple isolate servers: %s and %s',
525 self._storage.location, isolate_server)
526 return None
527 if self._storage.namespace != namespace:
528 logging.error(
529 'Task shards are using multiple namespaces: %s and %s',
530 self._storage.namespace, namespace)
531 return None
532 return self._storage
533
534
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500535def now():
536 """Exists so it can be mocked easily."""
537 return time.time()
538
539
maruel77f720b2015-09-15 12:35:22 -0700540def parse_time(value):
541 """Converts serialized time from the API to datetime.datetime."""
542 # When microseconds are 0, the '.123456' suffix is elided. This means the
543 # serialized format is not consistent, which confuses the hell out of python.
544 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
545 try:
546 return datetime.datetime.strptime(value, fmt)
547 except ValueError:
548 pass
549 raise ValueError('Failed to parse %s' % value)
550
551
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700552def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700553 base_url, shard_index, task_id, timeout, should_stop, output_collector,
554 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400555 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700556
Vadim Shtayurab450c602014-05-12 19:23:25 -0700557 Returns:
558 <result dict> on success.
559 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700560 """
maruel71c61c82016-02-22 06:52:05 -0800561 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700562 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700563 if include_perf:
564 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700565 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700566 started = now()
567 deadline = started + timeout if timeout else None
568 attempt = 0
569
570 while not should_stop.is_set():
571 attempt += 1
572
573 # Waiting for too long -> give up.
574 current_time = now()
575 if deadline and current_time >= deadline:
576 logging.error('retrieve_results(%s) timed out on attempt %d',
577 base_url, attempt)
578 return None
579
580 # Do not spin too fast. Spin faster at the beginning though.
581 # Start with 1 sec delay and for each 30 sec of waiting add another second
582 # of delay, until hitting 15 sec ceiling.
583 if attempt > 1:
584 max_delay = min(15, 1 + (current_time - started) / 30.0)
585 delay = min(max_delay, deadline - current_time) if deadline else max_delay
586 if delay > 0:
587 logging.debug('Waiting %.1f sec before retrying', delay)
588 should_stop.wait(delay)
589 if should_stop.is_set():
590 return None
591
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400592 # Disable internal retries in net.url_read_json, since we are doing retries
593 # ourselves.
594 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700595 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
596 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400597 result = net.url_read_json(result_url, retry_50x=False)
598 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400599 continue
maruel77f720b2015-09-15 12:35:22 -0700600
maruelbf53e042015-12-01 15:00:51 -0800601 if result.get('error'):
602 # An error occurred.
603 if result['error'].get('errors'):
604 for err in result['error']['errors']:
605 logging.warning(
606 'Error while reading task: %s; %s',
607 err.get('message'), err.get('debugInfo'))
608 elif result['error'].get('message'):
609 logging.warning(
610 'Error while reading task: %s', result['error']['message'])
611 continue
612
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400613 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700614 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400615 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700616 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700617 # Record the result, try to fetch attached output files (if any).
618 if output_collector:
619 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700620 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700621 if result.get('internal_failure'):
622 logging.error('Internal error!')
623 elif result['state'] == 'BOT_DIED':
624 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700625 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000626
627
maruel77f720b2015-09-15 12:35:22 -0700628def convert_to_old_format(result):
629 """Converts the task result data from Endpoints API format to old API format
630 for compatibility.
631
632 This goes into the file generated as --task-summary-json.
633 """
634 # Sets default.
635 result.setdefault('abandoned_ts', None)
636 result.setdefault('bot_id', None)
637 result.setdefault('bot_version', None)
638 result.setdefault('children_task_ids', [])
639 result.setdefault('completed_ts', None)
640 result.setdefault('cost_saved_usd', None)
641 result.setdefault('costs_usd', None)
642 result.setdefault('deduped_from', None)
643 result.setdefault('name', None)
644 result.setdefault('outputs_ref', None)
645 result.setdefault('properties_hash', None)
646 result.setdefault('server_versions', None)
647 result.setdefault('started_ts', None)
648 result.setdefault('tags', None)
649 result.setdefault('user', None)
650
651 # Convertion back to old API.
652 duration = result.pop('duration', None)
653 result['durations'] = [duration] if duration else []
654 exit_code = result.pop('exit_code', None)
655 result['exit_codes'] = [int(exit_code)] if exit_code else []
656 result['id'] = result.pop('task_id')
657 result['isolated_out'] = result.get('outputs_ref', None)
658 output = result.pop('output', None)
659 result['outputs'] = [output] if output else []
660 # properties_hash
661 # server_version
662 # Endpoints result 'state' as string. For compatibility with old code, convert
663 # to int.
664 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700665 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700666 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700667 if 'bot_dimensions' in result:
668 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700669 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700670 }
671 else:
672 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700673
674
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700675def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400676 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700677 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500678 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700680 Duplicate shards are ignored. Shards are yielded in order of completion.
681 Timed out shards are NOT yielded at all. Caller can compare number of yielded
682 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000683
684 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500685 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 +0000686 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500687
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700688 output_collector is an optional instance of TaskOutputCollector that will be
689 used to fetch files produced by a task from isolate server to the local disk.
690
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500691 Yields:
692 (index, result). In particular, 'result' is defined as the
693 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000694 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000695 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400696 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700697 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700698 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700699
maruel@chromium.org0437a732013-08-27 16:05:52 +0000700 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
701 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700702 # Adds a task to the thread pool to call 'retrieve_results' and return
703 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400704 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700705 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000706 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400707 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700708 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700709
710 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400711 for shard_index, task_id in enumerate(task_ids):
712 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700713
714 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400715 shards_remaining = range(len(task_ids))
716 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700717 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700718 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700719 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700720 shard_index, result = results_channel.pull(
721 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700722 except threading_utils.TaskChannel.Timeout:
723 if print_status_updates:
724 print(
725 'Waiting for results from the following shards: %s' %
726 ', '.join(map(str, shards_remaining)))
727 sys.stdout.flush()
728 continue
729 except Exception:
730 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700731
732 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700733 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000734 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500735 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000736 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700737
Vadim Shtayurab450c602014-05-12 19:23:25 -0700738 # Yield back results to the caller.
739 assert shard_index in shards_remaining
740 shards_remaining.remove(shard_index)
741 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700742
maruel@chromium.org0437a732013-08-27 16:05:52 +0000743 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700744 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000745 should_stop.set()
746
747
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400748def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000749 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700750 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400751 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700752 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
753 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400754 else:
755 pending = 'N/A'
756
maruel77f720b2015-09-15 12:35:22 -0700757 if metadata.get('duration') is not None:
758 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400759 else:
760 duration = 'N/A'
761
maruel77f720b2015-09-15 12:35:22 -0700762 if metadata.get('exit_code') is not None:
763 # Integers are encoded as string to not loose precision.
764 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400765 else:
766 exit_code = 'N/A'
767
768 bot_id = metadata.get('bot_id') or 'N/A'
769
maruel77f720b2015-09-15 12:35:22 -0700770 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400771 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400772 tag_footer = (
773 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
774 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400775
776 tag_len = max(len(tag_header), len(tag_footer))
777 dash_pad = '+-%s-+\n' % ('-' * tag_len)
778 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
779 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
780
781 header = dash_pad + tag_header + dash_pad
782 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700783 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400784 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000785
786
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700787def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700788 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700789 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700790 """Retrieves results of a Swarming task.
791
792 Returns:
793 process exit code that should be returned to the user.
794 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700795 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700796 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700797
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700798 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700799 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400800 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700801 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400802 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400803 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700804 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700805 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700806
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400807 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700808 shard_exit_code = metadata.get('exit_code')
809 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700810 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700811 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700812 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400813 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700814 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700815
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700816 if decorate:
leileied181762016-10-13 14:24:59 -0700817 s = decorate_shard_output(swarming, index, metadata).encode(
818 'utf-8', 'replace')
819 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400820 if len(seen_shards) < len(task_ids):
821 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700822 else:
maruel77f720b2015-09-15 12:35:22 -0700823 print('%s: %s %s' % (
824 metadata.get('bot_id', 'N/A'),
825 metadata['task_id'],
826 shard_exit_code))
827 if metadata['output']:
828 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400829 if output:
830 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700831 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700832 summary = output_collector.finalize()
833 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700834 # TODO(maruel): Make this optional.
835 for i in summary['shards']:
836 if i:
837 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700838 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700839
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400840 if decorate and total_duration:
841 print('Total duration: %.1fs' % total_duration)
842
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400843 if len(seen_shards) != len(task_ids):
844 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700845 print >> sys.stderr, ('Results from some shards are missing: %s' %
846 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700847 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700848
maruela5490782015-09-30 10:56:59 -0700849 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000850
851
maruel77f720b2015-09-15 12:35:22 -0700852### API management.
853
854
855class APIError(Exception):
856 pass
857
858
859def endpoints_api_discovery_apis(host):
860 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
861 the APIs exposed by a host.
862
863 https://developers.google.com/discovery/v1/reference/apis/list
864 """
maruel380e3262016-08-31 16:10:06 -0700865 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
866 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700867 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
868 if data is None:
869 raise APIError('Failed to discover APIs on %s' % host)
870 out = {}
871 for api in data['items']:
872 if api['id'] == 'discovery:v1':
873 continue
874 # URL is of the following form:
875 # url = host + (
876 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
877 api_data = net.url_read_json(api['discoveryRestUrl'])
878 if api_data is None:
879 raise APIError('Failed to discover %s on %s' % (api['id'], host))
880 out[api['id']] = api_data
881 return out
882
883
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500884### Commands.
885
886
887def abort_task(_swarming, _manifest):
888 """Given a task manifest that was triggered, aborts its execution."""
889 # TODO(vadimsh): No supported by the server yet.
890
891
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400892def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800893 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500894 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500895 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500896 dest='dimensions', metavar='FOO bar',
897 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500898 parser.add_option_group(parser.filter_group)
899
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400900
Vadim Shtayurab450c602014-05-12 19:23:25 -0700901def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400902 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700903 parser.sharding_group.add_option(
904 '--shards', type='int', default=1,
905 help='Number of shards to trigger and collect.')
906 parser.add_option_group(parser.sharding_group)
907
908
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909def add_trigger_options(parser):
910 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500911 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400912 add_filter_options(parser)
913
maruel681d6802017-01-17 16:56:03 -0800914 group = optparse.OptionGroup(parser, 'Task properties')
915 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500916 '-s', '--isolated',
917 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800918 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500919 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700920 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800921 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400922 '--idempotent', action='store_true', default=False,
923 help='When set, the server will actively try to find a previous task '
924 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800925 group.add_option(
iannuccieee1bca2016-10-28 13:16:23 -0700926 '--secret-bytes-path',
iannuccidc80dfb2016-10-28 12:50:20 -0700927 help='The optional path to a file containing the secret_bytes to use with'
928 'this task.')
maruel681d6802017-01-17 16:56:03 -0800929 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400930 '--hard-timeout', type='int', default=60*60,
931 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800932 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400933 '--io-timeout', type='int', default=20*60,
934 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800935 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500936 '--raw-cmd', action='store_true', default=False,
937 help='When set, the command after -- is used as-is without run_isolated. '
938 'In this case, no .isolated file is expected.')
maruel681d6802017-01-17 16:56:03 -0800939 group.add_option(
borenet02f772b2016-06-22 12:42:19 -0700940 '--cipd-package', action='append', default=[],
941 help='CIPD packages to install on the Swarming bot. Uses the format: '
942 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800943 group.add_option(
944 '--named-cache', action='append', nargs=2, default=[],
945 help='"<name> <relpath>" items to keep a persistent bot managed cache')
946 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700947 '--service-account',
948 help='Name of a service account to run the task as. Only literal "bot" '
949 'string can be specified currently (to run the task under bot\'s '
950 'account). Don\'t use task service accounts if not given '
951 '(default).')
maruel681d6802017-01-17 16:56:03 -0800952 group.add_option(
aludwincc5524e2016-10-28 10:25:24 -0700953 '-o', '--output', action='append', default=[],
954 help='A list of files to return in addition to those written to'
955 '$(ISOLATED_OUTDIR). An error will occur if a file specified by'
956 'this option is also written directly to $(ISOLATED_OUTDIR).')
maruel681d6802017-01-17 16:56:03 -0800957 parser.add_option_group(group)
958
959 group = optparse.OptionGroup(parser, 'Task request')
960 group.add_option(
961 '--priority', type='int', default=100,
962 help='The lower value, the more important the task is')
963 group.add_option(
964 '-T', '--task-name',
965 help='Display name of the task. Defaults to '
966 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
967 'isolated file is provided, if a hash is provided, it defaults to '
968 '<user>/<dimensions>/<isolated hash>/<timestamp>')
969 group.add_option(
970 '--tags', action='append', default=[],
971 help='Tags to assign to the task.')
972 group.add_option(
973 '--user', default='',
974 help='User associated with the task. Defaults to authenticated user on '
975 'the server.')
976 group.add_option(
977 '--expiration', type='int', default=6*60*60,
978 help='Seconds to allow the task to be pending for a bot to run before '
979 'this task request expires.')
980 group.add_option(
981 '--deadline', type='int', dest='expiration',
982 help=optparse.SUPPRESS_HELP)
983 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000984
985
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500986def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700987 """Processes trigger options and does preparatory steps.
988
989 Uploads files to isolate server and generates service account tokens if
990 necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500991 """
992 options.dimensions = dict(options.dimensions)
993 options.env = dict(options.env)
994
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500995 if not options.dimensions:
996 parser.error('Please at least specify one --dimension')
997 if options.raw_cmd:
998 if not args:
999 parser.error(
1000 'Arguments with --raw-cmd should be passed after -- as command '
1001 'delimiter.')
1002 if options.isolate_server:
1003 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
1004
1005 command = args
1006 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001007 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001008 options.user,
1009 '_'.join(
1010 '%s=%s' % (k, v)
1011 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -07001012 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001013 else:
nodir55be77b2016-05-03 09:39:57 -07001014 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001015 try:
maruel77f720b2015-09-15 12:35:22 -07001016 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001017 except ValueError as e:
1018 parser.error(str(e))
1019
borenet02f772b2016-06-22 12:42:19 -07001020 cipd_packages = []
1021 for p in options.cipd_package:
1022 split = p.split(':', 2)
1023 if len(split) != 3:
1024 parser.error('CIPD packages must take the form: path:package:version')
1025 cipd_packages.append(CipdPackage(
1026 package_name=split[1],
1027 path=split[0],
1028 version=split[2]))
1029 cipd_input = None
1030 if cipd_packages:
1031 cipd_input = CipdInput(
1032 client_package=None,
1033 packages=cipd_packages,
1034 server=None)
1035
iannuccidc80dfb2016-10-28 12:50:20 -07001036 secret_bytes = None
1037 if options.secret_bytes_path:
1038 with open(options.secret_bytes_path, 'r') as f:
1039 secret_bytes = f.read().encode('base64')
1040
maruel681d6802017-01-17 16:56:03 -08001041 caches = [
1042 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1043 for i in options.named_cache
1044 ]
nodir152cba62016-05-12 16:08:56 -07001045 # If inputs_ref.isolated is used, command is actually extra_args.
1046 # Otherwise it's an actual command to run.
1047 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -07001048 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001049 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001050 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -07001051 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001052 dimensions=options.dimensions,
1053 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001054 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -07001055 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -07001056 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001057 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001058 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001059 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001060 outputs=options.output,
1061 secret_bytes=secret_bytes)
maruel8fce7962015-10-21 11:17:47 -07001062 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1063 parser.error('--tags must be in the format key:value')
vadimsh93d167c2016-09-13 11:31:51 -07001064
1065 # Convert a service account email to a signed service account token to pass
1066 # to Swarming.
1067 service_account_token = None
1068 if options.service_account in ('bot', 'none'):
1069 service_account_token = options.service_account
1070 elif options.service_account:
1071 # pylint: disable=assignment-from-no-return
1072 service_account_token = mint_service_account_token(options.service_account)
1073
maruel77f720b2015-09-15 12:35:22 -07001074 return NewTaskRequest(
1075 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001077 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001078 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001079 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001080 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001081 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001082 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001083
1084
1085def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001086 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001087 '-t', '--timeout', type='float',
1088 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1089 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001090 parser.group_logging.add_option(
1091 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001092 parser.group_logging.add_option(
1093 '--print-status-updates', action='store_true',
1094 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001095 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001096 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001097 '--task-summary-json',
1098 metavar='FILE',
1099 help='Dump a summary of task results to this file as json. It contains '
1100 'only shards statuses as know to server directly. Any output files '
1101 'emitted by the task can be collected by using --task-output-dir')
1102 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001103 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001104 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001105 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001106 'directory contains per-shard directory with output files produced '
1107 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001108 parser.task_output_group.add_option(
1109 '--perf', action='store_true', default=False,
1110 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001111 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001112
1113
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001114@subcommand.usage('bots...')
1115def CMDbot_delete(parser, args):
1116 """Forcibly deletes bots from the Swarming server."""
1117 parser.add_option(
1118 '-f', '--force', action='store_true',
1119 help='Do not prompt for confirmation')
1120 options, args = parser.parse_args(args)
1121 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001122 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001123
1124 bots = sorted(args)
1125 if not options.force:
1126 print('Delete the following bots?')
1127 for bot in bots:
1128 print(' %s' % bot)
1129 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1130 print('Goodbye.')
1131 return 1
1132
1133 result = 0
1134 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001135 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001136 if net.url_read_json(url, data={}, method='POST') is None:
1137 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001138 result = 1
1139 return result
1140
1141
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001142def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001143 """Returns information about the bots connected to the Swarming server."""
1144 add_filter_options(parser)
1145 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001146 '--dead-only', action='store_true',
1147 help='Only print dead bots, useful to reap them and reimage broken bots')
1148 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001149 '-k', '--keep-dead', action='store_true',
1150 help='Do not filter out dead bots')
1151 parser.filter_group.add_option(
1152 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001153 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001154 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001155
1156 if options.keep_dead and options.dead_only:
1157 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001158
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001159 bots = []
1160 cursor = None
1161 limit = 250
1162 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001163 base_url = (
maruel380e3262016-08-31 16:10:06 -07001164 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001165 while True:
1166 url = base_url
1167 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001168 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001169 data = net.url_read_json(url)
1170 if data is None:
1171 print >> sys.stderr, 'Failed to access %s' % options.swarming
1172 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001173 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001174 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001175 if not cursor:
1176 break
1177
maruel77f720b2015-09-15 12:35:22 -07001178 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001179 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001180 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001181 continue
maruel77f720b2015-09-15 12:35:22 -07001182 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001183 continue
1184
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001185 # If the user requested to filter on dimensions, ensure the bot has all the
1186 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001187 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001188 for key, value in options.dimensions:
1189 if key not in dimensions:
1190 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001191 # A bot can have multiple value for a key, for example,
1192 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1193 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001194 if isinstance(dimensions[key], list):
1195 if value not in dimensions[key]:
1196 break
1197 else:
1198 if value != dimensions[key]:
1199 break
1200 else:
maruel77f720b2015-09-15 12:35:22 -07001201 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001202 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001203 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001204 if bot.get('task_id'):
1205 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001206 return 0
1207
1208
maruelfd0a90c2016-06-10 11:51:10 -07001209@subcommand.usage('task_id')
1210def CMDcancel(parser, args):
1211 """Cancels a task."""
1212 options, args = parser.parse_args(args)
1213 if not args:
1214 parser.error('Please specify the task to cancel')
1215 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001216 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001217 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1218 print('Deleting %s failed. Probably already gone' % task_id)
1219 return 1
1220 return 0
1221
1222
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001223@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001224def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001225 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001226
1227 The result can be in multiple part if the execution was sharded. It can
1228 potentially have retries.
1229 """
1230 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001231 parser.add_option(
1232 '-j', '--json',
1233 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001234 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001235 if not args and not options.json:
1236 parser.error('Must specify at least one task id or --json.')
1237 if args and options.json:
1238 parser.error('Only use one of task id or --json.')
1239
1240 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001241 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001242 try:
maruel1ceb3872015-10-14 06:10:44 -07001243 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001244 data = json.load(f)
1245 except (IOError, ValueError):
1246 parser.error('Failed to open %s' % options.json)
1247 try:
1248 tasks = sorted(
1249 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1250 args = [t['task_id'] for t in tasks]
1251 except (KeyError, TypeError):
1252 parser.error('Failed to process %s' % options.json)
1253 if options.timeout is None:
1254 options.timeout = (
1255 data['request']['properties']['execution_timeout_secs'] +
1256 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001257 else:
1258 valid = frozenset('0123456789abcdef')
1259 if any(not valid.issuperset(task_id) for task_id in args):
1260 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001261
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001262 try:
1263 return collect(
1264 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001265 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001266 options.timeout,
1267 options.decorate,
1268 options.print_status_updates,
1269 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001270 options.task_output_dir,
1271 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001272 except Failure:
1273 on_error.report(None)
1274 return 1
1275
1276
maruelbea00862015-09-18 09:55:36 -07001277@subcommand.usage('[filename]')
1278def CMDput_bootstrap(parser, args):
1279 """Uploads a new version of bootstrap.py."""
1280 options, args = parser.parse_args(args)
1281 if len(args) != 1:
1282 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001283 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001284 path = unicode(os.path.abspath(args[0]))
1285 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001286 content = f.read().decode('utf-8')
1287 data = net.url_read_json(url, data={'content': content})
1288 print data
1289 return 0
1290
1291
1292@subcommand.usage('[filename]')
1293def CMDput_bot_config(parser, args):
1294 """Uploads a new version of bot_config.py."""
1295 options, args = parser.parse_args(args)
1296 if len(args) != 1:
1297 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001298 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001299 path = unicode(os.path.abspath(args[0]))
1300 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001301 content = f.read().decode('utf-8')
1302 data = net.url_read_json(url, data={'content': content})
1303 print data
1304 return 0
1305
1306
maruel77f720b2015-09-15 12:35:22 -07001307@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001308def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001309 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1310 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001311
1312 Examples:
maruel77f720b2015-09-15 12:35:22 -07001313 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001314 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001315
maruel77f720b2015-09-15 12:35:22 -07001316 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001317 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1318
1319 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1320 quoting is important!:
1321 swarming.py query -S server-url.com --limit 10 \\
1322 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001323 """
1324 CHUNK_SIZE = 250
1325
1326 parser.add_option(
1327 '-L', '--limit', type='int', default=200,
1328 help='Limit to enforce on limitless items (like number of tasks); '
1329 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001330 parser.add_option(
1331 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001332 parser.add_option(
1333 '--progress', action='store_true',
1334 help='Prints a dot at each request to show progress')
1335 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001336 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001337 parser.error(
1338 'Must specify only method name and optionally query args properly '
1339 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001340 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001341 url = base_url
1342 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001343 # Check check, change if not working out.
1344 merge_char = '&' if '?' in url else '?'
1345 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001346 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001347 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001348 # TODO(maruel): Do basic diagnostic.
1349 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001350 return 1
1351
1352 # Some items support cursors. Try to get automatically if cursors are needed
1353 # by looking at the 'cursor' items.
1354 while (
1355 data.get('cursor') and
1356 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001357 merge_char = '&' if '?' in base_url else '?'
1358 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001359 if options.limit:
1360 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001361 if options.progress:
1362 sys.stdout.write('.')
1363 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001364 new = net.url_read_json(url)
1365 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001366 if options.progress:
1367 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001368 print >> sys.stderr, 'Failed to access %s' % options.swarming
1369 return 1
maruel81b37132015-10-21 06:42:13 -07001370 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001371 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001372
maruel77f720b2015-09-15 12:35:22 -07001373 if options.progress:
1374 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001375 if options.limit and len(data.get('items', [])) > options.limit:
1376 data['items'] = data['items'][:options.limit]
1377 data.pop('cursor', None)
1378
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001379 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001380 options.json = unicode(os.path.abspath(options.json))
1381 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001382 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001383 try:
maruel77f720b2015-09-15 12:35:22 -07001384 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001385 sys.stdout.write('\n')
1386 except IOError:
1387 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001388 return 0
1389
1390
maruel77f720b2015-09-15 12:35:22 -07001391def CMDquery_list(parser, args):
1392 """Returns list of all the Swarming APIs that can be used with command
1393 'query'.
1394 """
1395 parser.add_option(
1396 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1397 options, args = parser.parse_args(args)
1398 if args:
1399 parser.error('No argument allowed.')
1400
1401 try:
1402 apis = endpoints_api_discovery_apis(options.swarming)
1403 except APIError as e:
1404 parser.error(str(e))
1405 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001406 options.json = unicode(os.path.abspath(options.json))
1407 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001408 json.dump(apis, f)
1409 else:
1410 help_url = (
1411 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1412 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001413 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1414 if i:
1415 print('')
maruel77f720b2015-09-15 12:35:22 -07001416 print api_id
maruel11e31af2017-02-15 07:30:50 -08001417 print ' ' + api['description'].strip()
1418 if 'resources' in api:
1419 # Old.
1420 for j, (resource_name, resource) in enumerate(
1421 sorted(api['resources'].iteritems())):
1422 if j:
1423 print('')
1424 for method_name, method in sorted(resource['methods'].iteritems()):
1425 # Only list the GET ones.
1426 if method['httpMethod'] != 'GET':
1427 continue
1428 print '- %s.%s: %s' % (
1429 resource_name, method_name, method['path'])
1430 print('\n'.join(
1431 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1432 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1433 else:
1434 # New.
1435 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001436 # Only list the GET ones.
1437 if method['httpMethod'] != 'GET':
1438 continue
maruel11e31af2017-02-15 07:30:50 -08001439 print '- %s: %s' % (method['id'], method['path'])
1440 print('\n'.join(
1441 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001442 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1443 return 0
1444
1445
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001446@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001447def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001448 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001449
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001450 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001451 """
1452 add_trigger_options(parser)
1453 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001454 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001455 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001456 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001457 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001458 tasks = trigger_task_shards(
1459 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001460 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001461 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001462 'Failed to trigger %s(%s): %s' %
1463 (options.task_name, args[0], e.args[0]))
1464 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001465 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001466 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001467 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001468 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001469 task_ids = [
1470 t['task_id']
1471 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1472 ]
maruel71c61c82016-02-22 06:52:05 -08001473 if options.timeout is None:
1474 options.timeout = (
1475 task_request.properties.execution_timeout_secs +
1476 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001477 try:
1478 return collect(
1479 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001480 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001481 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001482 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001483 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001484 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001485 options.task_output_dir,
1486 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001487 except Failure:
1488 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001489 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001490
1491
maruel18122c62015-10-23 06:31:23 -07001492@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001493def CMDreproduce(parser, args):
1494 """Runs a task locally that was triggered on the server.
1495
1496 This running locally the same commands that have been run on the bot. The data
1497 downloaded will be in a subdirectory named 'work' of the current working
1498 directory.
maruel18122c62015-10-23 06:31:23 -07001499
1500 You can pass further additional arguments to the target command by passing
1501 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001502 """
maruelc070e672016-02-22 17:32:57 -08001503 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001504 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001505 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001506 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001507 extra_args = []
1508 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001509 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001510 if len(args) > 1:
1511 if args[1] == '--':
1512 if len(args) > 2:
1513 extra_args = args[2:]
1514 else:
1515 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001516
maruel380e3262016-08-31 16:10:06 -07001517 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001518 request = net.url_read_json(url)
1519 if not request:
1520 print >> sys.stderr, 'Failed to retrieve request data for the task'
1521 return 1
1522
maruel12e30012015-10-09 11:55:35 -07001523 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001524 if fs.isdir(workdir):
1525 parser.error('Please delete the directory \'work\' first')
1526 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001527 cachedir = unicode(os.path.abspath('cipd_cache'))
1528 if not fs.exists(cachedir):
1529 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001530
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001531 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001532 env = os.environ.copy()
1533 env['SWARMING_BOT_ID'] = 'reproduce'
1534 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001535 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001536 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001537 for i in properties['env']:
1538 key = i['key'].encode('utf-8')
1539 if not i['value']:
1540 env.pop(key, None)
1541 else:
1542 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001543
iannucci31ab9192017-05-02 19:11:56 -07001544 command = []
nodir152cba62016-05-12 16:08:56 -07001545 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001546 # Create the tree.
1547 with isolateserver.get_storage(
1548 properties['inputs_ref']['isolatedserver'],
1549 properties['inputs_ref']['namespace']) as storage:
1550 bundle = isolateserver.fetch_isolated(
1551 properties['inputs_ref']['isolated'],
1552 storage,
1553 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001554 workdir,
1555 False)
maruel29ab2fd2015-10-16 11:44:01 -07001556 command = bundle.command
1557 if bundle.relative_cwd:
1558 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001559 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001560
1561 if properties.get('command'):
1562 command.extend(properties['command'])
1563
1564 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1565 new_command = tools.fix_python_path(command)
1566 new_command = run_isolated.process_command(
1567 new_command, options.output_dir, None)
1568 if not options.output_dir and new_command != command:
1569 parser.error('The task has outputs, you must use --output-dir')
1570 command = new_command
1571 file_path.ensure_command_has_abs_path(command, workdir)
1572
1573 if properties.get('cipd_input'):
1574 ci = properties['cipd_input']
1575 cp = ci['client_package']
1576 client_manager = cipd.get_client(
1577 ci['server'], cp['package_name'], cp['version'], cachedir)
1578
1579 with client_manager as client:
1580 by_path = collections.defaultdict(list)
1581 for pkg in ci['packages']:
1582 path = pkg['path']
1583 # cipd deals with 'root' as ''
1584 if path == '.':
1585 path = ''
1586 by_path[path].append((pkg['package_name'], pkg['version']))
1587 client.ensure(workdir, by_path, cache_dir=cachedir)
1588
maruel77f720b2015-09-15 12:35:22 -07001589 try:
maruel18122c62015-10-23 06:31:23 -07001590 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001591 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001592 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001593 print >> sys.stderr, str(e)
1594 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001595
1596
maruel0eb1d1b2015-10-02 14:48:21 -07001597@subcommand.usage('bot_id')
1598def CMDterminate(parser, args):
1599 """Tells a bot to gracefully shut itself down as soon as it can.
1600
1601 This is done by completing whatever current task there is then exiting the bot
1602 process.
1603 """
1604 parser.add_option(
1605 '--wait', action='store_true', help='Wait for the bot to terminate')
1606 options, args = parser.parse_args(args)
1607 if len(args) != 1:
1608 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001609 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001610 request = net.url_read_json(url, data={})
1611 if not request:
1612 print >> sys.stderr, 'Failed to ask for termination'
1613 return 1
1614 if options.wait:
1615 return collect(
maruel9531ce02016-04-13 06:11:23 -07001616 options.swarming, [request['task_id']], 0., False, False, None, None,
1617 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001618 return 0
1619
1620
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001621@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001622def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001623 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001624
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001625 Accepts either the hash (sha1) of a .isolated file already uploaded or the
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001626 path to an .isolated file to archive.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001627
1628 If an .isolated file is specified instead of an hash, it is first archived.
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001629
1630 Passes all extra arguments provided after '--' as additional command line
1631 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001632 """
1633 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001634 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001635 parser.add_option(
1636 '--dump-json',
1637 metavar='FILE',
1638 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001639 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001640 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001641 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001642 tasks = trigger_task_shards(
1643 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001644 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001645 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001646 tasks_sorted = sorted(
1647 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001648 if options.dump_json:
1649 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001650 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001651 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001652 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001653 }
maruel46b015f2015-10-13 18:40:35 -07001654 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001655 print('To collect results, use:')
1656 print(' swarming.py collect -S %s --json %s' %
1657 (options.swarming, options.dump_json))
1658 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001659 print('To collect results, use:')
1660 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001661 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1662 print('Or visit:')
1663 for t in tasks_sorted:
1664 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001665 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001666 except Failure:
1667 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001668 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001669
1670
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001671class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001672 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001673 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001674 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001675 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001676 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001677 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001678 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001679 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001680 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001681 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001682
1683 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001684 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001685 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001686 auth.process_auth_options(self, options)
1687 user = self._process_swarming(options)
1688 if hasattr(options, 'user') and not options.user:
1689 options.user = user
1690 return options, args
1691
1692 def _process_swarming(self, options):
1693 """Processes the --swarming option and aborts if not specified.
1694
1695 Returns the identity as determined by the server.
1696 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001697 if not options.swarming:
1698 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001699 try:
1700 options.swarming = net.fix_url(options.swarming)
1701 except ValueError as e:
1702 self.error('--swarming %s' % e)
1703 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001704 try:
1705 user = auth.ensure_logged_in(options.swarming)
1706 except ValueError as e:
1707 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001708 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001709
1710
1711def main(args):
1712 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001713 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001714
1715
1716if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001717 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001718 fix_encoding.fix_encoding()
1719 tools.disable_buffering()
1720 colorama.init()
1721 sys.exit(main(sys.argv[1:]))