blob: 01f34d92f3ca029e8242d7b34c4c4a01cf9e1f5a [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
marueld9cc8422017-05-09 12:07:02 -07008__version__ = '0.9.0'
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
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050056def isolated_handle_options(options, args):
marueld9cc8422017-05-09 12:07:02 -070057 """Handles '--isolated <isolated>' and '-- <args...>' arguments.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050058
59 Returns:
maruel77f720b2015-09-15 12:35:22 -070060 tuple(command, inputs_ref).
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050061 """
62 isolated_cmd_args = []
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050063 if not options.isolated:
64 if '--' in args:
65 index = args.index('--')
66 isolated_cmd_args = args[index+1:]
67 args = args[:index]
68 else:
69 # optparse eats '--' sometimes.
70 isolated_cmd_args = args[1:]
71 args = args[:1]
72 if len(args) != 1:
73 raise ValueError(
74 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called '
75 'process.')
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050076 elif args:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050077 if '--' in args:
78 index = args.index('--')
79 isolated_cmd_args = args[index+1:]
80 if index != 0:
81 raise ValueError('Unexpected arguments.')
82 else:
83 # optparse eats '--' sometimes.
84 isolated_cmd_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050085
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050086 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050087 options.task_name = u'%s/%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070088 options.user,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050089 '_'.join(
90 '%s=%s' % (k, v)
91 for k, v in sorted(options.dimensions.iteritems())),
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -050092 options.isolated)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050093
maruel77f720b2015-09-15 12:35:22 -070094 inputs_ref = FilesRef(
nodir152cba62016-05-12 16:08:56 -070095 isolated=options.isolated,
96 isolatedserver=options.isolate_server,
97 namespace=options.namespace)
maruel77f720b2015-09-15 12:35:22 -070098 return isolated_cmd_args, inputs_ref
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050099
100
101### Triggering.
102
103
maruel77f720b2015-09-15 12:35:22 -0700104# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -0700105CipdPackage = collections.namedtuple(
106 'CipdPackage',
107 [
108 'package_name',
109 'path',
110 'version',
111 ])
112
113
114# See ../appengine/swarming/swarming_rpcs.py.
115CipdInput = collections.namedtuple(
116 'CipdInput',
117 [
118 'client_package',
119 'packages',
120 'server',
121 ])
122
123
124# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700125FilesRef = collections.namedtuple(
126 'FilesRef',
127 [
128 'isolated',
129 'isolatedserver',
130 'namespace',
131 ])
132
133
134# See ../appengine/swarming/swarming_rpcs.py.
135TaskProperties = collections.namedtuple(
136 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500137 [
maruel681d6802017-01-17 16:56:03 -0800138 'caches',
borenet02f772b2016-06-22 12:42:19 -0700139 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500140 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500141 'dimensions',
142 'env',
maruel77f720b2015-09-15 12:35:22 -0700143 'execution_timeout_secs',
144 'extra_args',
145 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500146 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700147 'inputs_ref',
148 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700149 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700150 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700151 ])
152
153
154# See ../appengine/swarming/swarming_rpcs.py.
155NewTaskRequest = collections.namedtuple(
156 'NewTaskRequest',
157 [
158 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500159 'name',
maruel77f720b2015-09-15 12:35:22 -0700160 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500161 'priority',
maruel77f720b2015-09-15 12:35:22 -0700162 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700163 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 'tags',
165 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500166 ])
167
168
maruel77f720b2015-09-15 12:35:22 -0700169def namedtuple_to_dict(value):
170 """Recursively converts a namedtuple to a dict."""
171 out = dict(value._asdict())
172 for k, v in out.iteritems():
173 if hasattr(v, '_asdict'):
174 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700175 elif isinstance(v, (list, tuple)):
176 l = []
177 for elem in v:
178 if hasattr(elem, '_asdict'):
179 l.append(namedtuple_to_dict(elem))
180 else:
181 l.append(elem)
182 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700183 return out
184
185
vadimsh93d167c2016-09-13 11:31:51 -0700186def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800187 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700188
189 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500190 """
maruel77f720b2015-09-15 12:35:22 -0700191 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700192 if hide_token:
193 if out['service_account_token'] not in (None, 'bot', 'none'):
194 out['service_account_token'] = '<hidden>'
195 # Don't send 'service_account_token' if it is None to avoid confusing older
196 # version of the server that doesn't know about 'service_account_token'.
197 if out['service_account_token'] in (None, 'none'):
198 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700199 # Maps are not supported until protobuf v3.
200 out['properties']['dimensions'] = [
201 {'key': k, 'value': v}
202 for k, v in out['properties']['dimensions'].iteritems()
203 ]
204 out['properties']['dimensions'].sort(key=lambda x: x['key'])
205 out['properties']['env'] = [
206 {'key': k, 'value': v}
207 for k, v in out['properties']['env'].iteritems()
208 ]
209 out['properties']['env'].sort(key=lambda x: x['key'])
210 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500211
212
maruel77f720b2015-09-15 12:35:22 -0700213def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500214 """Triggers a request on the Swarming server and returns the json data.
215
216 It's the low-level function.
217
218 Returns:
219 {
220 'request': {
221 'created_ts': u'2010-01-02 03:04:05',
222 'name': ..
223 },
224 'task_id': '12300',
225 }
226 """
227 logging.info('Triggering: %s', raw_request['name'])
228
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500229 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700230 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500231 if not result:
232 on_error.report('Failed to trigger task %s' % raw_request['name'])
233 return None
maruele557bce2015-11-17 09:01:27 -0800234 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800235 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800236 msg = 'Failed to trigger task %s' % raw_request['name']
237 if result['error'].get('errors'):
238 for err in result['error']['errors']:
239 if err.get('message'):
240 msg += '\nMessage: %s' % err['message']
241 if err.get('debugInfo'):
242 msg += '\nDebug info:\n%s' % err['debugInfo']
243 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800244 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800245
246 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800247 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 return result
249
250
251def setup_googletest(env, shards, index):
252 """Sets googletest specific environment variables."""
253 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700254 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
255 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
256 env = env[:]
257 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
258 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500259 return env
260
261
262def trigger_task_shards(swarming, task_request, shards):
263 """Triggers one or many subtasks of a sharded task.
264
265 Returns:
266 Dict with task details, returned to caller as part of --dump-json output.
267 None in case of failure.
268 """
269 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700270 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500271 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700272 req['properties']['env'] = setup_googletest(
273 req['properties']['env'], shards, index)
274 req['name'] += ':%s:%s' % (index, shards)
275 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500276
277 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500278 tasks = {}
279 priority_warning = False
280 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700281 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500282 if not task:
283 break
284 logging.info('Request result: %s', task)
285 if (not priority_warning and
286 task['request']['priority'] != task_request.priority):
287 priority_warning = True
288 print >> sys.stderr, (
289 'Priority was reset to %s' % task['request']['priority'])
290 tasks[request['name']] = {
291 'shard_index': index,
292 'task_id': task['task_id'],
293 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
294 }
295
296 # Some shards weren't triggered. Abort everything.
297 if len(tasks) != len(requests):
298 if tasks:
299 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
300 len(tasks), len(requests))
301 for task_dict in tasks.itervalues():
302 abort_task(swarming, task_dict['task_id'])
303 return None
304
305 return tasks
306
307
vadimsh93d167c2016-09-13 11:31:51 -0700308def mint_service_account_token(service_account):
309 """Given a service account name returns a delegation token for this account.
310
311 The token is generated based on triggering user's credentials. It is passed
312 to Swarming, that uses it when running tasks.
313 """
314 logging.info(
315 'Generating delegation token for service account "%s"', service_account)
316 raise NotImplementedError('Custom service accounts are not implemented yet')
317
318
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500319### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000320
321
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700322# How often to print status updates to stdout in 'collect'.
323STATUS_UPDATE_INTERVAL = 15 * 60.
324
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400325
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400326class State(object):
327 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000328
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400329 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
330 values are part of the API so if they change, the API changed.
331
332 It's in fact an enum. Values should be in decreasing order of importance.
333 """
334 RUNNING = 0x10
335 PENDING = 0x20
336 EXPIRED = 0x30
337 TIMED_OUT = 0x40
338 BOT_DIED = 0x50
339 CANCELED = 0x60
340 COMPLETED = 0x70
341
maruel77f720b2015-09-15 12:35:22 -0700342 STATES = (
343 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
344 'COMPLETED')
345 STATES_RUNNING = ('RUNNING', 'PENDING')
346 STATES_NOT_RUNNING = (
347 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
348 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
349 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400350
351 _NAMES = {
352 RUNNING: 'Running',
353 PENDING: 'Pending',
354 EXPIRED: 'Expired',
355 TIMED_OUT: 'Execution timed out',
356 BOT_DIED: 'Bot died',
357 CANCELED: 'User canceled',
358 COMPLETED: 'Completed',
359 }
360
maruel77f720b2015-09-15 12:35:22 -0700361 _ENUMS = {
362 'RUNNING': RUNNING,
363 'PENDING': PENDING,
364 'EXPIRED': EXPIRED,
365 'TIMED_OUT': TIMED_OUT,
366 'BOT_DIED': BOT_DIED,
367 'CANCELED': CANCELED,
368 'COMPLETED': COMPLETED,
369 }
370
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400371 @classmethod
372 def to_string(cls, state):
373 """Returns a user-readable string representing a State."""
374 if state not in cls._NAMES:
375 raise ValueError('Invalid state %s' % state)
376 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000377
maruel77f720b2015-09-15 12:35:22 -0700378 @classmethod
379 def from_enum(cls, state):
380 """Returns int value based on the string."""
381 if state not in cls._ENUMS:
382 raise ValueError('Invalid state %s' % state)
383 return cls._ENUMS[state]
384
maruel@chromium.org0437a732013-08-27 16:05:52 +0000385
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700387 """Assembles task execution summary (for --task-summary-json output).
388
389 Optionally fetches task outputs from isolate server to local disk (used when
390 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700391
392 This object is shared among multiple threads running 'retrieve_results'
393 function, in particular they call 'process_shard_result' method in parallel.
394 """
395
maruel0eb1d1b2015-10-02 14:48:21 -0700396 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700397 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
398
399 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700400 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700401 shard_count: expected number of task shards.
402 """
maruel12e30012015-10-09 11:55:35 -0700403 self.task_output_dir = (
404 unicode(os.path.abspath(task_output_dir))
405 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700406 self.shard_count = shard_count
407
408 self._lock = threading.Lock()
409 self._per_shard_results = {}
410 self._storage = None
411
nodire5028a92016-04-29 14:38:21 -0700412 if self.task_output_dir:
413 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700414
Vadim Shtayurab450c602014-05-12 19:23:25 -0700415 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700416 """Stores results of a single task shard, fetches output files if necessary.
417
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400418 Modifies |result| in place.
419
maruel77f720b2015-09-15 12:35:22 -0700420 shard_index is 0-based.
421
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700422 Called concurrently from multiple threads.
423 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700425 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700426 if shard_index < 0 or shard_index >= self.shard_count:
427 logging.warning(
428 'Shard index %d is outside of expected range: [0; %d]',
429 shard_index, self.shard_count - 1)
430 return
431
maruel77f720b2015-09-15 12:35:22 -0700432 if result.get('outputs_ref'):
433 ref = result['outputs_ref']
434 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
435 ref['isolatedserver'],
436 urllib.urlencode(
437 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400438
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439 # Store result dict of that shard, ignore results we've already seen.
440 with self._lock:
441 if shard_index in self._per_shard_results:
442 logging.warning('Ignoring duplicate shard index %d', shard_index)
443 return
444 self._per_shard_results[shard_index] = result
445
446 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700447 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400448 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700449 result['outputs_ref']['isolatedserver'],
450 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400451 if storage:
452 # Output files are supposed to be small and they are not reused across
453 # tasks. So use MemoryCache for them instead of on-disk cache. Make
454 # files writable, so that calling script can delete them.
455 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700456 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400457 storage,
458 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700459 os.path.join(self.task_output_dir, str(shard_index)),
460 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700461
462 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700463 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700464 with self._lock:
465 # Write an array of shard results with None for missing shards.
466 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700467 'shards': [
468 self._per_shard_results.get(i) for i in xrange(self.shard_count)
469 ],
470 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700471 # Write summary.json to task_output_dir as well.
472 if self.task_output_dir:
473 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700474 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700475 summary,
476 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700477 if self._storage:
478 self._storage.close()
479 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700480 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700481
482 def _get_storage(self, isolate_server, namespace):
483 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700484 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700485 with self._lock:
486 if not self._storage:
487 self._storage = isolateserver.get_storage(isolate_server, namespace)
488 else:
489 # Shards must all use exact same isolate server and namespace.
490 if self._storage.location != isolate_server:
491 logging.error(
492 'Task shards are using multiple isolate servers: %s and %s',
493 self._storage.location, isolate_server)
494 return None
495 if self._storage.namespace != namespace:
496 logging.error(
497 'Task shards are using multiple namespaces: %s and %s',
498 self._storage.namespace, namespace)
499 return None
500 return self._storage
501
502
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500503def now():
504 """Exists so it can be mocked easily."""
505 return time.time()
506
507
maruel77f720b2015-09-15 12:35:22 -0700508def parse_time(value):
509 """Converts serialized time from the API to datetime.datetime."""
510 # When microseconds are 0, the '.123456' suffix is elided. This means the
511 # serialized format is not consistent, which confuses the hell out of python.
512 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
513 try:
514 return datetime.datetime.strptime(value, fmt)
515 except ValueError:
516 pass
517 raise ValueError('Failed to parse %s' % value)
518
519
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700520def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700521 base_url, shard_index, task_id, timeout, should_stop, output_collector,
522 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400523 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700524
Vadim Shtayurab450c602014-05-12 19:23:25 -0700525 Returns:
526 <result dict> on success.
527 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700528 """
maruel71c61c82016-02-22 06:52:05 -0800529 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700530 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700531 if include_perf:
532 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700533 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700534 started = now()
535 deadline = started + timeout if timeout else None
536 attempt = 0
537
538 while not should_stop.is_set():
539 attempt += 1
540
541 # Waiting for too long -> give up.
542 current_time = now()
543 if deadline and current_time >= deadline:
544 logging.error('retrieve_results(%s) timed out on attempt %d',
545 base_url, attempt)
546 return None
547
548 # Do not spin too fast. Spin faster at the beginning though.
549 # Start with 1 sec delay and for each 30 sec of waiting add another second
550 # of delay, until hitting 15 sec ceiling.
551 if attempt > 1:
552 max_delay = min(15, 1 + (current_time - started) / 30.0)
553 delay = min(max_delay, deadline - current_time) if deadline else max_delay
554 if delay > 0:
555 logging.debug('Waiting %.1f sec before retrying', delay)
556 should_stop.wait(delay)
557 if should_stop.is_set():
558 return None
559
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400560 # Disable internal retries in net.url_read_json, since we are doing retries
561 # ourselves.
562 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700563 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
564 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400565 result = net.url_read_json(result_url, retry_50x=False)
566 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400567 continue
maruel77f720b2015-09-15 12:35:22 -0700568
maruelbf53e042015-12-01 15:00:51 -0800569 if result.get('error'):
570 # An error occurred.
571 if result['error'].get('errors'):
572 for err in result['error']['errors']:
573 logging.warning(
574 'Error while reading task: %s; %s',
575 err.get('message'), err.get('debugInfo'))
576 elif result['error'].get('message'):
577 logging.warning(
578 'Error while reading task: %s', result['error']['message'])
579 continue
580
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400581 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700582 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400583 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700584 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700585 # Record the result, try to fetch attached output files (if any).
586 if output_collector:
587 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700588 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700589 if result.get('internal_failure'):
590 logging.error('Internal error!')
591 elif result['state'] == 'BOT_DIED':
592 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700593 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000594
595
maruel77f720b2015-09-15 12:35:22 -0700596def convert_to_old_format(result):
597 """Converts the task result data from Endpoints API format to old API format
598 for compatibility.
599
600 This goes into the file generated as --task-summary-json.
601 """
602 # Sets default.
603 result.setdefault('abandoned_ts', None)
604 result.setdefault('bot_id', None)
605 result.setdefault('bot_version', None)
606 result.setdefault('children_task_ids', [])
607 result.setdefault('completed_ts', None)
608 result.setdefault('cost_saved_usd', None)
609 result.setdefault('costs_usd', None)
610 result.setdefault('deduped_from', None)
611 result.setdefault('name', None)
612 result.setdefault('outputs_ref', None)
613 result.setdefault('properties_hash', None)
614 result.setdefault('server_versions', None)
615 result.setdefault('started_ts', None)
616 result.setdefault('tags', None)
617 result.setdefault('user', None)
618
619 # Convertion back to old API.
620 duration = result.pop('duration', None)
621 result['durations'] = [duration] if duration else []
622 exit_code = result.pop('exit_code', None)
623 result['exit_codes'] = [int(exit_code)] if exit_code else []
624 result['id'] = result.pop('task_id')
625 result['isolated_out'] = result.get('outputs_ref', None)
626 output = result.pop('output', None)
627 result['outputs'] = [output] if output else []
628 # properties_hash
629 # server_version
630 # Endpoints result 'state' as string. For compatibility with old code, convert
631 # to int.
632 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700633 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700634 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700635 if 'bot_dimensions' in result:
636 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700637 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700638 }
639 else:
640 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700641
642
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700643def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400644 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700645 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500646 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000647
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700648 Duplicate shards are ignored. Shards are yielded in order of completion.
649 Timed out shards are NOT yielded at all. Caller can compare number of yielded
650 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000651
652 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500653 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 +0000654 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500655
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700656 output_collector is an optional instance of TaskOutputCollector that will be
657 used to fetch files produced by a task from isolate server to the local disk.
658
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500659 Yields:
660 (index, result). In particular, 'result' is defined as the
661 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000662 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000663 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400664 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700665 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700666 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700667
maruel@chromium.org0437a732013-08-27 16:05:52 +0000668 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
669 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700670 # Adds a task to the thread pool to call 'retrieve_results' and return
671 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400672 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700673 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000674 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400675 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700676 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700677
678 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400679 for shard_index, task_id in enumerate(task_ids):
680 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700681
682 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400683 shards_remaining = range(len(task_ids))
684 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700685 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700686 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700687 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700688 shard_index, result = results_channel.pull(
689 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700690 except threading_utils.TaskChannel.Timeout:
691 if print_status_updates:
692 print(
693 'Waiting for results from the following shards: %s' %
694 ', '.join(map(str, shards_remaining)))
695 sys.stdout.flush()
696 continue
697 except Exception:
698 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700699
700 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700701 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000702 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500703 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000704 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700705
Vadim Shtayurab450c602014-05-12 19:23:25 -0700706 # Yield back results to the caller.
707 assert shard_index in shards_remaining
708 shards_remaining.remove(shard_index)
709 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700710
maruel@chromium.org0437a732013-08-27 16:05:52 +0000711 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700712 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000713 should_stop.set()
714
715
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400716def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000717 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700718 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400719 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700720 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
721 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400722 else:
723 pending = 'N/A'
724
maruel77f720b2015-09-15 12:35:22 -0700725 if metadata.get('duration') is not None:
726 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400727 else:
728 duration = 'N/A'
729
maruel77f720b2015-09-15 12:35:22 -0700730 if metadata.get('exit_code') is not None:
731 # Integers are encoded as string to not loose precision.
732 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400733 else:
734 exit_code = 'N/A'
735
736 bot_id = metadata.get('bot_id') or 'N/A'
737
maruel77f720b2015-09-15 12:35:22 -0700738 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400739 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400740 tag_footer = (
741 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
742 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400743
744 tag_len = max(len(tag_header), len(tag_footer))
745 dash_pad = '+-%s-+\n' % ('-' * tag_len)
746 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
747 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
748
749 header = dash_pad + tag_header + dash_pad
750 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700751 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400752 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000753
754
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700755def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700756 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700757 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700758 """Retrieves results of a Swarming task.
759
760 Returns:
761 process exit code that should be returned to the user.
762 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700763 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700764 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700765
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700766 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700767 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400768 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400770 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400771 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700772 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700774
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400775 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700776 shard_exit_code = metadata.get('exit_code')
777 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700778 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700779 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700780 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400781 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700782 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700783
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700784 if decorate:
leileied181762016-10-13 14:24:59 -0700785 s = decorate_shard_output(swarming, index, metadata).encode(
786 'utf-8', 'replace')
787 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400788 if len(seen_shards) < len(task_ids):
789 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790 else:
maruel77f720b2015-09-15 12:35:22 -0700791 print('%s: %s %s' % (
792 metadata.get('bot_id', 'N/A'),
793 metadata['task_id'],
794 shard_exit_code))
795 if metadata['output']:
796 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400797 if output:
798 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700799 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700800 summary = output_collector.finalize()
801 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700802 # TODO(maruel): Make this optional.
803 for i in summary['shards']:
804 if i:
805 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700806 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700807
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400808 if decorate and total_duration:
809 print('Total duration: %.1fs' % total_duration)
810
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400811 if len(seen_shards) != len(task_ids):
812 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700813 print >> sys.stderr, ('Results from some shards are missing: %s' %
814 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700815 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700816
maruela5490782015-09-30 10:56:59 -0700817 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000818
819
maruel77f720b2015-09-15 12:35:22 -0700820### API management.
821
822
823class APIError(Exception):
824 pass
825
826
827def endpoints_api_discovery_apis(host):
828 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
829 the APIs exposed by a host.
830
831 https://developers.google.com/discovery/v1/reference/apis/list
832 """
maruel380e3262016-08-31 16:10:06 -0700833 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
834 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700835 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
836 if data is None:
837 raise APIError('Failed to discover APIs on %s' % host)
838 out = {}
839 for api in data['items']:
840 if api['id'] == 'discovery:v1':
841 continue
842 # URL is of the following form:
843 # url = host + (
844 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
845 api_data = net.url_read_json(api['discoveryRestUrl'])
846 if api_data is None:
847 raise APIError('Failed to discover %s on %s' % (api['id'], host))
848 out[api['id']] = api_data
849 return out
850
851
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500852### Commands.
853
854
855def abort_task(_swarming, _manifest):
856 """Given a task manifest that was triggered, aborts its execution."""
857 # TODO(vadimsh): No supported by the server yet.
858
859
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400860def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800861 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500862 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500863 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500864 dest='dimensions', metavar='FOO bar',
865 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500866 parser.add_option_group(parser.filter_group)
867
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400868
Vadim Shtayurab450c602014-05-12 19:23:25 -0700869def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400870 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700871 parser.sharding_group.add_option(
872 '--shards', type='int', default=1,
873 help='Number of shards to trigger and collect.')
874 parser.add_option_group(parser.sharding_group)
875
876
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400877def add_trigger_options(parser):
878 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500879 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400880 add_filter_options(parser)
881
maruel681d6802017-01-17 16:56:03 -0800882 group = optparse.OptionGroup(parser, 'Task properties')
883 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500884 '-s', '--isolated',
885 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800886 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500887 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700888 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800889 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400890 '--idempotent', action='store_true', default=False,
891 help='When set, the server will actively try to find a previous task '
892 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800893 group.add_option(
iannuccieee1bca2016-10-28 13:16:23 -0700894 '--secret-bytes-path',
iannuccidc80dfb2016-10-28 12:50:20 -0700895 help='The optional path to a file containing the secret_bytes to use with'
896 'this task.')
maruel681d6802017-01-17 16:56:03 -0800897 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400898 '--hard-timeout', type='int', default=60*60,
899 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800900 group.add_option(
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400901 '--io-timeout', type='int', default=20*60,
902 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800903 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500904 '--raw-cmd', action='store_true', default=False,
905 help='When set, the command after -- is used as-is without run_isolated. '
906 'In this case, no .isolated file is expected.')
maruel681d6802017-01-17 16:56:03 -0800907 group.add_option(
borenet02f772b2016-06-22 12:42:19 -0700908 '--cipd-package', action='append', default=[],
909 help='CIPD packages to install on the Swarming bot. Uses the format: '
910 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800911 group.add_option(
912 '--named-cache', action='append', nargs=2, default=[],
913 help='"<name> <relpath>" items to keep a persistent bot managed cache')
914 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700915 '--service-account',
916 help='Name of a service account to run the task as. Only literal "bot" '
917 'string can be specified currently (to run the task under bot\'s '
918 'account). Don\'t use task service accounts if not given '
919 '(default).')
maruel681d6802017-01-17 16:56:03 -0800920 group.add_option(
aludwincc5524e2016-10-28 10:25:24 -0700921 '-o', '--output', action='append', default=[],
922 help='A list of files to return in addition to those written to'
923 '$(ISOLATED_OUTDIR). An error will occur if a file specified by'
924 'this option is also written directly to $(ISOLATED_OUTDIR).')
maruel681d6802017-01-17 16:56:03 -0800925 parser.add_option_group(group)
926
927 group = optparse.OptionGroup(parser, 'Task request')
928 group.add_option(
929 '--priority', type='int', default=100,
930 help='The lower value, the more important the task is')
931 group.add_option(
932 '-T', '--task-name',
933 help='Display name of the task. Defaults to '
934 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
935 'isolated file is provided, if a hash is provided, it defaults to '
936 '<user>/<dimensions>/<isolated hash>/<timestamp>')
937 group.add_option(
938 '--tags', action='append', default=[],
939 help='Tags to assign to the task.')
940 group.add_option(
941 '--user', default='',
942 help='User associated with the task. Defaults to authenticated user on '
943 'the server.')
944 group.add_option(
945 '--expiration', type='int', default=6*60*60,
946 help='Seconds to allow the task to be pending for a bot to run before '
947 'this task request expires.')
948 group.add_option(
949 '--deadline', type='int', dest='expiration',
950 help=optparse.SUPPRESS_HELP)
951 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000952
953
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500954def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700955 """Processes trigger options and does preparatory steps.
956
marueld9cc8422017-05-09 12:07:02 -0700957 Generates service account tokens if necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500958 """
959 options.dimensions = dict(options.dimensions)
960 options.env = dict(options.env)
961
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500962 if not options.dimensions:
963 parser.error('Please at least specify one --dimension')
964 if options.raw_cmd:
965 if not args:
966 parser.error(
967 'Arguments with --raw-cmd should be passed after -- as command '
968 'delimiter.')
969 if options.isolate_server:
970 parser.error('Can\'t use both --raw-cmd and --isolate-server.')
971
972 command = args
973 if not options.task_name:
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500974 options.task_name = u'%s/%s' % (
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500975 options.user,
976 '_'.join(
977 '%s=%s' % (k, v)
978 for k, v in sorted(options.dimensions.iteritems())))
maruel77f720b2015-09-15 12:35:22 -0700979 inputs_ref = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500980 else:
nodir55be77b2016-05-03 09:39:57 -0700981 isolateserver.process_isolate_server_options(parser, options, False, True)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500982 try:
maruel77f720b2015-09-15 12:35:22 -0700983 command, inputs_ref = isolated_handle_options(options, args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500984 except ValueError as e:
985 parser.error(str(e))
986
borenet02f772b2016-06-22 12:42:19 -0700987 cipd_packages = []
988 for p in options.cipd_package:
989 split = p.split(':', 2)
990 if len(split) != 3:
991 parser.error('CIPD packages must take the form: path:package:version')
992 cipd_packages.append(CipdPackage(
993 package_name=split[1],
994 path=split[0],
995 version=split[2]))
996 cipd_input = None
997 if cipd_packages:
998 cipd_input = CipdInput(
999 client_package=None,
1000 packages=cipd_packages,
1001 server=None)
1002
iannuccidc80dfb2016-10-28 12:50:20 -07001003 secret_bytes = None
1004 if options.secret_bytes_path:
1005 with open(options.secret_bytes_path, 'r') as f:
1006 secret_bytes = f.read().encode('base64')
1007
maruel681d6802017-01-17 16:56:03 -08001008 caches = [
1009 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1010 for i in options.named_cache
1011 ]
nodir152cba62016-05-12 16:08:56 -07001012 # If inputs_ref.isolated is used, command is actually extra_args.
1013 # Otherwise it's an actual command to run.
1014 isolated_input = inputs_ref and inputs_ref.isolated
maruel77f720b2015-09-15 12:35:22 -07001015 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001016 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001017 cipd_input=cipd_input,
nodir152cba62016-05-12 16:08:56 -07001018 command=None if isolated_input else command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001019 dimensions=options.dimensions,
1020 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001021 execution_timeout_secs=options.hard_timeout,
nodir152cba62016-05-12 16:08:56 -07001022 extra_args=command if isolated_input else None,
maruel77f720b2015-09-15 12:35:22 -07001023 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001024 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001025 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001026 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001027 outputs=options.output,
1028 secret_bytes=secret_bytes)
maruel8fce7962015-10-21 11:17:47 -07001029 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1030 parser.error('--tags must be in the format key:value')
vadimsh93d167c2016-09-13 11:31:51 -07001031
1032 # Convert a service account email to a signed service account token to pass
1033 # to Swarming.
1034 service_account_token = None
1035 if options.service_account in ('bot', 'none'):
1036 service_account_token = options.service_account
1037 elif options.service_account:
1038 # pylint: disable=assignment-from-no-return
1039 service_account_token = mint_service_account_token(options.service_account)
1040
maruel77f720b2015-09-15 12:35:22 -07001041 return NewTaskRequest(
1042 expiration_secs=options.expiration,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001043 name=options.task_name,
maruel77f720b2015-09-15 12:35:22 -07001044 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001045 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001046 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001047 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001048 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001049 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001050
1051
1052def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001053 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001054 '-t', '--timeout', type='float',
1055 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1056 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001057 parser.group_logging.add_option(
1058 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001059 parser.group_logging.add_option(
1060 '--print-status-updates', action='store_true',
1061 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001062 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001063 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001064 '--task-summary-json',
1065 metavar='FILE',
1066 help='Dump a summary of task results to this file as json. It contains '
1067 'only shards statuses as know to server directly. Any output files '
1068 'emitted by the task can be collected by using --task-output-dir')
1069 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001070 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001071 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001072 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001073 'directory contains per-shard directory with output files produced '
1074 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001075 parser.task_output_group.add_option(
1076 '--perf', action='store_true', default=False,
1077 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001078 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001079
1080
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001081@subcommand.usage('bots...')
1082def CMDbot_delete(parser, args):
1083 """Forcibly deletes bots from the Swarming server."""
1084 parser.add_option(
1085 '-f', '--force', action='store_true',
1086 help='Do not prompt for confirmation')
1087 options, args = parser.parse_args(args)
1088 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001089 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001090
1091 bots = sorted(args)
1092 if not options.force:
1093 print('Delete the following bots?')
1094 for bot in bots:
1095 print(' %s' % bot)
1096 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1097 print('Goodbye.')
1098 return 1
1099
1100 result = 0
1101 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001102 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001103 if net.url_read_json(url, data={}, method='POST') is None:
1104 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001105 result = 1
1106 return result
1107
1108
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001109def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001110 """Returns information about the bots connected to the Swarming server."""
1111 add_filter_options(parser)
1112 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001113 '--dead-only', action='store_true',
1114 help='Only print dead bots, useful to reap them and reimage broken bots')
1115 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001116 '-k', '--keep-dead', action='store_true',
1117 help='Do not filter out dead bots')
1118 parser.filter_group.add_option(
1119 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001120 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001121 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001122
1123 if options.keep_dead and options.dead_only:
1124 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001125
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001126 bots = []
1127 cursor = None
1128 limit = 250
1129 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001130 base_url = (
maruel380e3262016-08-31 16:10:06 -07001131 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001132 while True:
1133 url = base_url
1134 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001135 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001136 data = net.url_read_json(url)
1137 if data is None:
1138 print >> sys.stderr, 'Failed to access %s' % options.swarming
1139 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001140 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001141 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001142 if not cursor:
1143 break
1144
maruel77f720b2015-09-15 12:35:22 -07001145 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001146 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001147 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001148 continue
maruel77f720b2015-09-15 12:35:22 -07001149 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001150 continue
1151
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001152 # If the user requested to filter on dimensions, ensure the bot has all the
1153 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001154 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001155 for key, value in options.dimensions:
1156 if key not in dimensions:
1157 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001158 # A bot can have multiple value for a key, for example,
1159 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1160 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001161 if isinstance(dimensions[key], list):
1162 if value not in dimensions[key]:
1163 break
1164 else:
1165 if value != dimensions[key]:
1166 break
1167 else:
maruel77f720b2015-09-15 12:35:22 -07001168 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001169 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001170 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001171 if bot.get('task_id'):
1172 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001173 return 0
1174
1175
maruelfd0a90c2016-06-10 11:51:10 -07001176@subcommand.usage('task_id')
1177def CMDcancel(parser, args):
1178 """Cancels a task."""
1179 options, args = parser.parse_args(args)
1180 if not args:
1181 parser.error('Please specify the task to cancel')
1182 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001183 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001184 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1185 print('Deleting %s failed. Probably already gone' % task_id)
1186 return 1
1187 return 0
1188
1189
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001190@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001191def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001192 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001193
1194 The result can be in multiple part if the execution was sharded. It can
1195 potentially have retries.
1196 """
1197 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001198 parser.add_option(
1199 '-j', '--json',
1200 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001201 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001202 if not args and not options.json:
1203 parser.error('Must specify at least one task id or --json.')
1204 if args and options.json:
1205 parser.error('Only use one of task id or --json.')
1206
1207 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001208 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001209 try:
maruel1ceb3872015-10-14 06:10:44 -07001210 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001211 data = json.load(f)
1212 except (IOError, ValueError):
1213 parser.error('Failed to open %s' % options.json)
1214 try:
1215 tasks = sorted(
1216 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1217 args = [t['task_id'] for t in tasks]
1218 except (KeyError, TypeError):
1219 parser.error('Failed to process %s' % options.json)
1220 if options.timeout is None:
1221 options.timeout = (
1222 data['request']['properties']['execution_timeout_secs'] +
1223 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001224 else:
1225 valid = frozenset('0123456789abcdef')
1226 if any(not valid.issuperset(task_id) for task_id in args):
1227 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001228
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001229 try:
1230 return collect(
1231 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001232 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001233 options.timeout,
1234 options.decorate,
1235 options.print_status_updates,
1236 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001237 options.task_output_dir,
1238 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001239 except Failure:
1240 on_error.report(None)
1241 return 1
1242
1243
maruelbea00862015-09-18 09:55:36 -07001244@subcommand.usage('[filename]')
1245def CMDput_bootstrap(parser, args):
1246 """Uploads a new version of bootstrap.py."""
1247 options, args = parser.parse_args(args)
1248 if len(args) != 1:
1249 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001250 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001251 path = unicode(os.path.abspath(args[0]))
1252 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001253 content = f.read().decode('utf-8')
1254 data = net.url_read_json(url, data={'content': content})
1255 print data
1256 return 0
1257
1258
1259@subcommand.usage('[filename]')
1260def CMDput_bot_config(parser, args):
1261 """Uploads a new version of bot_config.py."""
1262 options, args = parser.parse_args(args)
1263 if len(args) != 1:
1264 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001265 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001266 path = unicode(os.path.abspath(args[0]))
1267 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001268 content = f.read().decode('utf-8')
1269 data = net.url_read_json(url, data={'content': content})
1270 print data
1271 return 0
1272
1273
maruel77f720b2015-09-15 12:35:22 -07001274@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001275def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001276 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1277 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001278
1279 Examples:
maruel77f720b2015-09-15 12:35:22 -07001280 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001281 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001282
maruel77f720b2015-09-15 12:35:22 -07001283 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001284 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1285
1286 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1287 quoting is important!:
1288 swarming.py query -S server-url.com --limit 10 \\
1289 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001290 """
1291 CHUNK_SIZE = 250
1292
1293 parser.add_option(
1294 '-L', '--limit', type='int', default=200,
1295 help='Limit to enforce on limitless items (like number of tasks); '
1296 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001297 parser.add_option(
1298 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001299 parser.add_option(
1300 '--progress', action='store_true',
1301 help='Prints a dot at each request to show progress')
1302 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001303 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001304 parser.error(
1305 'Must specify only method name and optionally query args properly '
1306 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001307 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001308 url = base_url
1309 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001310 # Check check, change if not working out.
1311 merge_char = '&' if '?' in url else '?'
1312 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001313 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001314 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001315 # TODO(maruel): Do basic diagnostic.
1316 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001317 return 1
1318
1319 # Some items support cursors. Try to get automatically if cursors are needed
1320 # by looking at the 'cursor' items.
1321 while (
1322 data.get('cursor') and
1323 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001324 merge_char = '&' if '?' in base_url else '?'
1325 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001326 if options.limit:
1327 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001328 if options.progress:
1329 sys.stdout.write('.')
1330 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001331 new = net.url_read_json(url)
1332 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001333 if options.progress:
1334 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001335 print >> sys.stderr, 'Failed to access %s' % options.swarming
1336 return 1
maruel81b37132015-10-21 06:42:13 -07001337 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001338 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001339
maruel77f720b2015-09-15 12:35:22 -07001340 if options.progress:
1341 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001342 if options.limit and len(data.get('items', [])) > options.limit:
1343 data['items'] = data['items'][:options.limit]
1344 data.pop('cursor', None)
1345
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001346 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001347 options.json = unicode(os.path.abspath(options.json))
1348 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001349 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001350 try:
maruel77f720b2015-09-15 12:35:22 -07001351 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001352 sys.stdout.write('\n')
1353 except IOError:
1354 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001355 return 0
1356
1357
maruel77f720b2015-09-15 12:35:22 -07001358def CMDquery_list(parser, args):
1359 """Returns list of all the Swarming APIs that can be used with command
1360 'query'.
1361 """
1362 parser.add_option(
1363 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1364 options, args = parser.parse_args(args)
1365 if args:
1366 parser.error('No argument allowed.')
1367
1368 try:
1369 apis = endpoints_api_discovery_apis(options.swarming)
1370 except APIError as e:
1371 parser.error(str(e))
1372 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001373 options.json = unicode(os.path.abspath(options.json))
1374 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001375 json.dump(apis, f)
1376 else:
1377 help_url = (
1378 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1379 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001380 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1381 if i:
1382 print('')
maruel77f720b2015-09-15 12:35:22 -07001383 print api_id
maruel11e31af2017-02-15 07:30:50 -08001384 print ' ' + api['description'].strip()
1385 if 'resources' in api:
1386 # Old.
1387 for j, (resource_name, resource) in enumerate(
1388 sorted(api['resources'].iteritems())):
1389 if j:
1390 print('')
1391 for method_name, method in sorted(resource['methods'].iteritems()):
1392 # Only list the GET ones.
1393 if method['httpMethod'] != 'GET':
1394 continue
1395 print '- %s.%s: %s' % (
1396 resource_name, method_name, method['path'])
1397 print('\n'.join(
1398 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1399 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1400 else:
1401 # New.
1402 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001403 # Only list the GET ones.
1404 if method['httpMethod'] != 'GET':
1405 continue
maruel11e31af2017-02-15 07:30:50 -08001406 print '- %s: %s' % (method['id'], method['path'])
1407 print('\n'.join(
1408 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001409 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1410 return 0
1411
1412
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001413@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001414def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001415 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001416
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001417 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001418 """
1419 add_trigger_options(parser)
1420 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001421 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001422 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001423 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001424 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001425 tasks = trigger_task_shards(
1426 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001427 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001428 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001429 'Failed to trigger %s(%s): %s' %
1430 (options.task_name, args[0], e.args[0]))
1431 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001432 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001433 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001434 return 1
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001435 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001436 task_ids = [
1437 t['task_id']
1438 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1439 ]
maruel71c61c82016-02-22 06:52:05 -08001440 if options.timeout is None:
1441 options.timeout = (
1442 task_request.properties.execution_timeout_secs +
1443 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001444 try:
1445 return collect(
1446 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001447 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001448 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001449 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001450 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001451 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001452 options.task_output_dir,
1453 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001454 except Failure:
1455 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001456 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001457
1458
maruel18122c62015-10-23 06:31:23 -07001459@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001460def CMDreproduce(parser, args):
1461 """Runs a task locally that was triggered on the server.
1462
1463 This running locally the same commands that have been run on the bot. The data
1464 downloaded will be in a subdirectory named 'work' of the current working
1465 directory.
maruel18122c62015-10-23 06:31:23 -07001466
1467 You can pass further additional arguments to the target command by passing
1468 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001469 """
maruelc070e672016-02-22 17:32:57 -08001470 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001471 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001472 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001473 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001474 extra_args = []
1475 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001476 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001477 if len(args) > 1:
1478 if args[1] == '--':
1479 if len(args) > 2:
1480 extra_args = args[2:]
1481 else:
1482 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001483
maruel380e3262016-08-31 16:10:06 -07001484 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001485 request = net.url_read_json(url)
1486 if not request:
1487 print >> sys.stderr, 'Failed to retrieve request data for the task'
1488 return 1
1489
maruel12e30012015-10-09 11:55:35 -07001490 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001491 if fs.isdir(workdir):
1492 parser.error('Please delete the directory \'work\' first')
1493 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001494 cachedir = unicode(os.path.abspath('cipd_cache'))
1495 if not fs.exists(cachedir):
1496 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001497
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001498 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001499 env = os.environ.copy()
1500 env['SWARMING_BOT_ID'] = 'reproduce'
1501 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001502 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001503 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001504 for i in properties['env']:
1505 key = i['key'].encode('utf-8')
1506 if not i['value']:
1507 env.pop(key, None)
1508 else:
1509 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001510
iannucci31ab9192017-05-02 19:11:56 -07001511 command = []
nodir152cba62016-05-12 16:08:56 -07001512 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001513 # Create the tree.
1514 with isolateserver.get_storage(
1515 properties['inputs_ref']['isolatedserver'],
1516 properties['inputs_ref']['namespace']) as storage:
1517 bundle = isolateserver.fetch_isolated(
1518 properties['inputs_ref']['isolated'],
1519 storage,
1520 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001521 workdir,
1522 False)
maruel29ab2fd2015-10-16 11:44:01 -07001523 command = bundle.command
1524 if bundle.relative_cwd:
1525 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001526 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001527
1528 if properties.get('command'):
1529 command.extend(properties['command'])
1530
1531 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1532 new_command = tools.fix_python_path(command)
1533 new_command = run_isolated.process_command(
1534 new_command, options.output_dir, None)
1535 if not options.output_dir and new_command != command:
1536 parser.error('The task has outputs, you must use --output-dir')
1537 command = new_command
1538 file_path.ensure_command_has_abs_path(command, workdir)
1539
1540 if properties.get('cipd_input'):
1541 ci = properties['cipd_input']
1542 cp = ci['client_package']
1543 client_manager = cipd.get_client(
1544 ci['server'], cp['package_name'], cp['version'], cachedir)
1545
1546 with client_manager as client:
1547 by_path = collections.defaultdict(list)
1548 for pkg in ci['packages']:
1549 path = pkg['path']
1550 # cipd deals with 'root' as ''
1551 if path == '.':
1552 path = ''
1553 by_path[path].append((pkg['package_name'], pkg['version']))
1554 client.ensure(workdir, by_path, cache_dir=cachedir)
1555
maruel77f720b2015-09-15 12:35:22 -07001556 try:
maruel18122c62015-10-23 06:31:23 -07001557 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001558 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001559 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001560 print >> sys.stderr, str(e)
1561 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001562
1563
maruel0eb1d1b2015-10-02 14:48:21 -07001564@subcommand.usage('bot_id')
1565def CMDterminate(parser, args):
1566 """Tells a bot to gracefully shut itself down as soon as it can.
1567
1568 This is done by completing whatever current task there is then exiting the bot
1569 process.
1570 """
1571 parser.add_option(
1572 '--wait', action='store_true', help='Wait for the bot to terminate')
1573 options, args = parser.parse_args(args)
1574 if len(args) != 1:
1575 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001576 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001577 request = net.url_read_json(url, data={})
1578 if not request:
1579 print >> sys.stderr, 'Failed to ask for termination'
1580 return 1
1581 if options.wait:
1582 return collect(
maruel9531ce02016-04-13 06:11:23 -07001583 options.swarming, [request['task_id']], 0., False, False, None, None,
1584 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001585 return 0
1586
1587
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001588@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001589def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001590 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001591
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001592 Passes all extra arguments provided after '--' as additional command line
1593 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001594 """
1595 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001596 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001597 parser.add_option(
1598 '--dump-json',
1599 metavar='FILE',
1600 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001601 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001602 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001603 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001604 tasks = trigger_task_shards(
1605 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001606 if tasks:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001607 print('Triggered task: %s' % options.task_name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001608 tasks_sorted = sorted(
1609 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001610 if options.dump_json:
1611 data = {
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001612 'base_task_name': options.task_name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001613 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001614 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001615 }
maruel46b015f2015-10-13 18:40:35 -07001616 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001617 print('To collect results, use:')
1618 print(' swarming.py collect -S %s --json %s' %
1619 (options.swarming, options.dump_json))
1620 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001621 print('To collect results, use:')
1622 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001623 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1624 print('Or visit:')
1625 for t in tasks_sorted:
1626 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001627 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001628 except Failure:
1629 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001630 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001631
1632
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001633class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001634 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001635 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001636 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001637 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001638 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001639 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001640 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001641 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001642 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001643 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001644
1645 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001646 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001647 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001648 auth.process_auth_options(self, options)
1649 user = self._process_swarming(options)
1650 if hasattr(options, 'user') and not options.user:
1651 options.user = user
1652 return options, args
1653
1654 def _process_swarming(self, options):
1655 """Processes the --swarming option and aborts if not specified.
1656
1657 Returns the identity as determined by the server.
1658 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001659 if not options.swarming:
1660 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001661 try:
1662 options.swarming = net.fix_url(options.swarming)
1663 except ValueError as e:
1664 self.error('--swarming %s' % e)
1665 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001666 try:
1667 user = auth.ensure_logged_in(options.swarming)
1668 except ValueError as e:
1669 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001670 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001671
1672
1673def main(args):
1674 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001675 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001676
1677
1678if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001679 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001680 fix_encoding.fix_encoding()
1681 tools.disable_buffering()
1682 colorama.init()
1683 sys.exit(main(sys.argv[1:]))