blob: 6514eea5529f4257041aadea3f160ca8bc2bbc97 [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
maruela9fe2cb2017-05-10 10:43:23 -07008__version__ = '0.9.1'
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
maruela9fe2cb2017-05-10 10:43:23 -070053def default_task_name(options):
54 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050055 if not options.task_name:
maruela9fe2cb2017-05-10 10:43:23 -070056 task_name = u'%s/%s' % (
maruel4e901792017-05-09 12:07:02 -070057 options.user,
maruel0165e822017-06-08 06:26:53 -070058 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruela9fe2cb2017-05-10 10:43:23 -070059 if options.isolated:
60 task_name += u'/' + options.isolated
61 return task_name
62 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050063
64
65### Triggering.
66
67
maruel77f720b2015-09-15 12:35:22 -070068# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070069CipdPackage = collections.namedtuple(
70 'CipdPackage',
71 [
72 'package_name',
73 'path',
74 'version',
75 ])
76
77
78# See ../appengine/swarming/swarming_rpcs.py.
79CipdInput = collections.namedtuple(
80 'CipdInput',
81 [
82 'client_package',
83 'packages',
84 'server',
85 ])
86
87
88# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070089FilesRef = collections.namedtuple(
90 'FilesRef',
91 [
92 'isolated',
93 'isolatedserver',
94 'namespace',
95 ])
96
97
98# See ../appengine/swarming/swarming_rpcs.py.
99TaskProperties = collections.namedtuple(
100 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500101 [
maruel681d6802017-01-17 16:56:03 -0800102 'caches',
borenet02f772b2016-06-22 12:42:19 -0700103 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500104 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500105 'dimensions',
106 'env',
maruel77f720b2015-09-15 12:35:22 -0700107 'execution_timeout_secs',
108 'extra_args',
109 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700111 'inputs_ref',
112 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700113 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700114 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700115 ])
116
117
118# See ../appengine/swarming/swarming_rpcs.py.
119NewTaskRequest = collections.namedtuple(
120 'NewTaskRequest',
121 [
122 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500123 'name',
maruel77f720b2015-09-15 12:35:22 -0700124 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500125 'priority',
maruel77f720b2015-09-15 12:35:22 -0700126 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700127 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500128 'tags',
129 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500130 ])
131
132
maruel77f720b2015-09-15 12:35:22 -0700133def namedtuple_to_dict(value):
134 """Recursively converts a namedtuple to a dict."""
135 out = dict(value._asdict())
136 for k, v in out.iteritems():
137 if hasattr(v, '_asdict'):
138 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700139 elif isinstance(v, (list, tuple)):
140 l = []
141 for elem in v:
142 if hasattr(elem, '_asdict'):
143 l.append(namedtuple_to_dict(elem))
144 else:
145 l.append(elem)
146 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700147 return out
148
149
vadimsh93d167c2016-09-13 11:31:51 -0700150def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800151 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700152
153 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500154 """
maruel77f720b2015-09-15 12:35:22 -0700155 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700156 if hide_token:
157 if out['service_account_token'] not in (None, 'bot', 'none'):
158 out['service_account_token'] = '<hidden>'
159 # Don't send 'service_account_token' if it is None to avoid confusing older
160 # version of the server that doesn't know about 'service_account_token'.
161 if out['service_account_token'] in (None, 'none'):
162 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700163 out['properties']['dimensions'] = [
164 {'key': k, 'value': v}
maruel0165e822017-06-08 06:26:53 -0700165 for k, v in out['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700166 ]
maruel77f720b2015-09-15 12:35:22 -0700167 out['properties']['env'] = [
168 {'key': k, 'value': v}
169 for k, v in out['properties']['env'].iteritems()
170 ]
171 out['properties']['env'].sort(key=lambda x: x['key'])
172 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500173
174
maruel77f720b2015-09-15 12:35:22 -0700175def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500176 """Triggers a request on the Swarming server and returns the json data.
177
178 It's the low-level function.
179
180 Returns:
181 {
182 'request': {
183 'created_ts': u'2010-01-02 03:04:05',
184 'name': ..
185 },
186 'task_id': '12300',
187 }
188 """
189 logging.info('Triggering: %s', raw_request['name'])
190
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500191 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700192 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500193 if not result:
194 on_error.report('Failed to trigger task %s' % raw_request['name'])
195 return None
maruele557bce2015-11-17 09:01:27 -0800196 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800197 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800198 msg = 'Failed to trigger task %s' % raw_request['name']
199 if result['error'].get('errors'):
200 for err in result['error']['errors']:
201 if err.get('message'):
202 msg += '\nMessage: %s' % err['message']
203 if err.get('debugInfo'):
204 msg += '\nDebug info:\n%s' % err['debugInfo']
205 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800206 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800207
208 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800209 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500210 return result
211
212
213def setup_googletest(env, shards, index):
214 """Sets googletest specific environment variables."""
215 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700216 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
217 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
218 env = env[:]
219 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
220 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500221 return env
222
223
224def trigger_task_shards(swarming, task_request, shards):
225 """Triggers one or many subtasks of a sharded task.
226
227 Returns:
228 Dict with task details, returned to caller as part of --dump-json output.
229 None in case of failure.
230 """
231 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700232 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500233 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700234 req['properties']['env'] = setup_googletest(
235 req['properties']['env'], shards, index)
236 req['name'] += ':%s:%s' % (index, shards)
237 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500238
239 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500240 tasks = {}
241 priority_warning = False
242 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700243 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500244 if not task:
245 break
246 logging.info('Request result: %s', task)
247 if (not priority_warning and
248 task['request']['priority'] != task_request.priority):
249 priority_warning = True
250 print >> sys.stderr, (
251 'Priority was reset to %s' % task['request']['priority'])
252 tasks[request['name']] = {
253 'shard_index': index,
254 'task_id': task['task_id'],
255 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
256 }
257
258 # Some shards weren't triggered. Abort everything.
259 if len(tasks) != len(requests):
260 if tasks:
261 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
262 len(tasks), len(requests))
263 for task_dict in tasks.itervalues():
264 abort_task(swarming, task_dict['task_id'])
265 return None
266
267 return tasks
268
269
vadimsh93d167c2016-09-13 11:31:51 -0700270def mint_service_account_token(service_account):
271 """Given a service account name returns a delegation token for this account.
272
273 The token is generated based on triggering user's credentials. It is passed
274 to Swarming, that uses it when running tasks.
275 """
276 logging.info(
277 'Generating delegation token for service account "%s"', service_account)
278 raise NotImplementedError('Custom service accounts are not implemented yet')
279
280
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500281### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000282
283
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700284# How often to print status updates to stdout in 'collect'.
285STATUS_UPDATE_INTERVAL = 15 * 60.
286
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400287
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400288class State(object):
289 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000290
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400291 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
292 values are part of the API so if they change, the API changed.
293
294 It's in fact an enum. Values should be in decreasing order of importance.
295 """
296 RUNNING = 0x10
297 PENDING = 0x20
298 EXPIRED = 0x30
299 TIMED_OUT = 0x40
300 BOT_DIED = 0x50
301 CANCELED = 0x60
302 COMPLETED = 0x70
303
maruel77f720b2015-09-15 12:35:22 -0700304 STATES = (
305 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
306 'COMPLETED')
307 STATES_RUNNING = ('RUNNING', 'PENDING')
308 STATES_NOT_RUNNING = (
309 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
310 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
311 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400312
313 _NAMES = {
314 RUNNING: 'Running',
315 PENDING: 'Pending',
316 EXPIRED: 'Expired',
317 TIMED_OUT: 'Execution timed out',
318 BOT_DIED: 'Bot died',
319 CANCELED: 'User canceled',
320 COMPLETED: 'Completed',
321 }
322
maruel77f720b2015-09-15 12:35:22 -0700323 _ENUMS = {
324 'RUNNING': RUNNING,
325 'PENDING': PENDING,
326 'EXPIRED': EXPIRED,
327 'TIMED_OUT': TIMED_OUT,
328 'BOT_DIED': BOT_DIED,
329 'CANCELED': CANCELED,
330 'COMPLETED': COMPLETED,
331 }
332
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400333 @classmethod
334 def to_string(cls, state):
335 """Returns a user-readable string representing a State."""
336 if state not in cls._NAMES:
337 raise ValueError('Invalid state %s' % state)
338 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000339
maruel77f720b2015-09-15 12:35:22 -0700340 @classmethod
341 def from_enum(cls, state):
342 """Returns int value based on the string."""
343 if state not in cls._ENUMS:
344 raise ValueError('Invalid state %s' % state)
345 return cls._ENUMS[state]
346
maruel@chromium.org0437a732013-08-27 16:05:52 +0000347
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700348class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700349 """Assembles task execution summary (for --task-summary-json output).
350
351 Optionally fetches task outputs from isolate server to local disk (used when
352 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353
354 This object is shared among multiple threads running 'retrieve_results'
355 function, in particular they call 'process_shard_result' method in parallel.
356 """
357
maruel0eb1d1b2015-10-02 14:48:21 -0700358 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700359 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
360
361 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700362 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 shard_count: expected number of task shards.
364 """
maruel12e30012015-10-09 11:55:35 -0700365 self.task_output_dir = (
366 unicode(os.path.abspath(task_output_dir))
367 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368 self.shard_count = shard_count
369
370 self._lock = threading.Lock()
371 self._per_shard_results = {}
372 self._storage = None
373
nodire5028a92016-04-29 14:38:21 -0700374 if self.task_output_dir:
375 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376
Vadim Shtayurab450c602014-05-12 19:23:25 -0700377 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700378 """Stores results of a single task shard, fetches output files if necessary.
379
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400380 Modifies |result| in place.
381
maruel77f720b2015-09-15 12:35:22 -0700382 shard_index is 0-based.
383
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 Called concurrently from multiple threads.
385 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700387 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700388 if shard_index < 0 or shard_index >= self.shard_count:
389 logging.warning(
390 'Shard index %d is outside of expected range: [0; %d]',
391 shard_index, self.shard_count - 1)
392 return
393
maruel77f720b2015-09-15 12:35:22 -0700394 if result.get('outputs_ref'):
395 ref = result['outputs_ref']
396 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
397 ref['isolatedserver'],
398 urllib.urlencode(
399 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400400
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700401 # Store result dict of that shard, ignore results we've already seen.
402 with self._lock:
403 if shard_index in self._per_shard_results:
404 logging.warning('Ignoring duplicate shard index %d', shard_index)
405 return
406 self._per_shard_results[shard_index] = result
407
408 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700409 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400410 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700411 result['outputs_ref']['isolatedserver'],
412 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400413 if storage:
414 # Output files are supposed to be small and they are not reused across
415 # tasks. So use MemoryCache for them instead of on-disk cache. Make
416 # files writable, so that calling script can delete them.
417 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700418 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400419 storage,
420 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700421 os.path.join(self.task_output_dir, str(shard_index)),
422 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700423
424 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700425 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700426 with self._lock:
427 # Write an array of shard results with None for missing shards.
428 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700429 'shards': [
430 self._per_shard_results.get(i) for i in xrange(self.shard_count)
431 ],
432 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700433 # Write summary.json to task_output_dir as well.
434 if self.task_output_dir:
435 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700436 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700437 summary,
438 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439 if self._storage:
440 self._storage.close()
441 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443
444 def _get_storage(self, isolate_server, namespace):
445 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700446 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447 with self._lock:
448 if not self._storage:
449 self._storage = isolateserver.get_storage(isolate_server, namespace)
450 else:
451 # Shards must all use exact same isolate server and namespace.
452 if self._storage.location != isolate_server:
453 logging.error(
454 'Task shards are using multiple isolate servers: %s and %s',
455 self._storage.location, isolate_server)
456 return None
457 if self._storage.namespace != namespace:
458 logging.error(
459 'Task shards are using multiple namespaces: %s and %s',
460 self._storage.namespace, namespace)
461 return None
462 return self._storage
463
464
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500465def now():
466 """Exists so it can be mocked easily."""
467 return time.time()
468
469
maruel77f720b2015-09-15 12:35:22 -0700470def parse_time(value):
471 """Converts serialized time from the API to datetime.datetime."""
472 # When microseconds are 0, the '.123456' suffix is elided. This means the
473 # serialized format is not consistent, which confuses the hell out of python.
474 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
475 try:
476 return datetime.datetime.strptime(value, fmt)
477 except ValueError:
478 pass
479 raise ValueError('Failed to parse %s' % value)
480
481
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700482def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700483 base_url, shard_index, task_id, timeout, should_stop, output_collector,
484 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400485 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700486
Vadim Shtayurab450c602014-05-12 19:23:25 -0700487 Returns:
488 <result dict> on success.
489 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700490 """
maruel71c61c82016-02-22 06:52:05 -0800491 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700492 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700493 if include_perf:
494 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700495 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700496 started = now()
497 deadline = started + timeout if timeout else None
498 attempt = 0
499
500 while not should_stop.is_set():
501 attempt += 1
502
503 # Waiting for too long -> give up.
504 current_time = now()
505 if deadline and current_time >= deadline:
506 logging.error('retrieve_results(%s) timed out on attempt %d',
507 base_url, attempt)
508 return None
509
510 # Do not spin too fast. Spin faster at the beginning though.
511 # Start with 1 sec delay and for each 30 sec of waiting add another second
512 # of delay, until hitting 15 sec ceiling.
513 if attempt > 1:
514 max_delay = min(15, 1 + (current_time - started) / 30.0)
515 delay = min(max_delay, deadline - current_time) if deadline else max_delay
516 if delay > 0:
517 logging.debug('Waiting %.1f sec before retrying', delay)
518 should_stop.wait(delay)
519 if should_stop.is_set():
520 return None
521
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400522 # Disable internal retries in net.url_read_json, since we are doing retries
523 # ourselves.
524 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700525 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
526 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400527 result = net.url_read_json(result_url, retry_50x=False)
528 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400529 continue
maruel77f720b2015-09-15 12:35:22 -0700530
maruelbf53e042015-12-01 15:00:51 -0800531 if result.get('error'):
532 # An error occurred.
533 if result['error'].get('errors'):
534 for err in result['error']['errors']:
535 logging.warning(
536 'Error while reading task: %s; %s',
537 err.get('message'), err.get('debugInfo'))
538 elif result['error'].get('message'):
539 logging.warning(
540 'Error while reading task: %s', result['error']['message'])
541 continue
542
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400543 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700544 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400545 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700546 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700547 # Record the result, try to fetch attached output files (if any).
548 if output_collector:
549 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700550 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700551 if result.get('internal_failure'):
552 logging.error('Internal error!')
553 elif result['state'] == 'BOT_DIED':
554 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700555 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000556
557
maruel77f720b2015-09-15 12:35:22 -0700558def convert_to_old_format(result):
559 """Converts the task result data from Endpoints API format to old API format
560 for compatibility.
561
562 This goes into the file generated as --task-summary-json.
563 """
564 # Sets default.
565 result.setdefault('abandoned_ts', None)
566 result.setdefault('bot_id', None)
567 result.setdefault('bot_version', None)
568 result.setdefault('children_task_ids', [])
569 result.setdefault('completed_ts', None)
570 result.setdefault('cost_saved_usd', None)
571 result.setdefault('costs_usd', None)
572 result.setdefault('deduped_from', None)
573 result.setdefault('name', None)
574 result.setdefault('outputs_ref', None)
575 result.setdefault('properties_hash', None)
576 result.setdefault('server_versions', None)
577 result.setdefault('started_ts', None)
578 result.setdefault('tags', None)
579 result.setdefault('user', None)
580
581 # Convertion back to old API.
582 duration = result.pop('duration', None)
583 result['durations'] = [duration] if duration else []
584 exit_code = result.pop('exit_code', None)
585 result['exit_codes'] = [int(exit_code)] if exit_code else []
586 result['id'] = result.pop('task_id')
587 result['isolated_out'] = result.get('outputs_ref', None)
588 output = result.pop('output', None)
589 result['outputs'] = [output] if output else []
590 # properties_hash
591 # server_version
592 # Endpoints result 'state' as string. For compatibility with old code, convert
593 # to int.
594 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700595 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700596 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700597 if 'bot_dimensions' in result:
598 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700599 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700600 }
601 else:
602 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700603
604
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700605def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400606 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700607 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500608 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000609
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700610 Duplicate shards are ignored. Shards are yielded in order of completion.
611 Timed out shards are NOT yielded at all. Caller can compare number of yielded
612 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000613
614 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500615 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 +0000616 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500617
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700618 output_collector is an optional instance of TaskOutputCollector that will be
619 used to fetch files produced by a task from isolate server to the local disk.
620
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500621 Yields:
622 (index, result). In particular, 'result' is defined as the
623 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000625 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400626 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700627 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700628 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700629
maruel@chromium.org0437a732013-08-27 16:05:52 +0000630 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
631 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700632 # Adds a task to the thread pool to call 'retrieve_results' and return
633 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400634 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700635 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000636 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400637 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700638 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639
640 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 for shard_index, task_id in enumerate(task_ids):
642 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643
644 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400645 shards_remaining = range(len(task_ids))
646 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700647 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700648 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 shard_index, result = results_channel.pull(
651 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700652 except threading_utils.TaskChannel.Timeout:
653 if print_status_updates:
654 print(
655 'Waiting for results from the following shards: %s' %
656 ', '.join(map(str, shards_remaining)))
657 sys.stdout.flush()
658 continue
659 except Exception:
660 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700661
662 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000664 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500665 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000666 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700667
Vadim Shtayurab450c602014-05-12 19:23:25 -0700668 # Yield back results to the caller.
669 assert shard_index in shards_remaining
670 shards_remaining.remove(shard_index)
671 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700672
maruel@chromium.org0437a732013-08-27 16:05:52 +0000673 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 should_stop.set()
676
677
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400678def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700680 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400681 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700682 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
683 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400684 else:
685 pending = 'N/A'
686
maruel77f720b2015-09-15 12:35:22 -0700687 if metadata.get('duration') is not None:
688 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400689 else:
690 duration = 'N/A'
691
maruel77f720b2015-09-15 12:35:22 -0700692 if metadata.get('exit_code') is not None:
693 # Integers are encoded as string to not loose precision.
694 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400695 else:
696 exit_code = 'N/A'
697
698 bot_id = metadata.get('bot_id') or 'N/A'
699
maruel77f720b2015-09-15 12:35:22 -0700700 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400701 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400702 tag_footer = (
703 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
704 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400705
706 tag_len = max(len(tag_header), len(tag_footer))
707 dash_pad = '+-%s-+\n' % ('-' * tag_len)
708 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
709 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
710
711 header = dash_pad + tag_header + dash_pad
712 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700713 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400714 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000715
716
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700717def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700718 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700719 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700720 """Retrieves results of a Swarming task.
721
722 Returns:
723 process exit code that should be returned to the user.
724 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700725 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700726 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700727
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700728 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700729 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400730 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400732 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400733 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700734 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700735 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700736
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400737 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700738 shard_exit_code = metadata.get('exit_code')
739 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700740 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700741 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700742 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400743 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700744 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700745
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746 if decorate:
leileied181762016-10-13 14:24:59 -0700747 s = decorate_shard_output(swarming, index, metadata).encode(
748 'utf-8', 'replace')
749 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400750 if len(seen_shards) < len(task_ids):
751 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700752 else:
maruel77f720b2015-09-15 12:35:22 -0700753 print('%s: %s %s' % (
754 metadata.get('bot_id', 'N/A'),
755 metadata['task_id'],
756 shard_exit_code))
757 if metadata['output']:
758 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400759 if output:
760 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700761 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700762 summary = output_collector.finalize()
763 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700764 # TODO(maruel): Make this optional.
765 for i in summary['shards']:
766 if i:
767 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700768 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400770 if decorate and total_duration:
771 print('Total duration: %.1fs' % total_duration)
772
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400773 if len(seen_shards) != len(task_ids):
774 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700775 print >> sys.stderr, ('Results from some shards are missing: %s' %
776 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700777 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700778
maruela5490782015-09-30 10:56:59 -0700779 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000780
781
maruel77f720b2015-09-15 12:35:22 -0700782### API management.
783
784
785class APIError(Exception):
786 pass
787
788
789def endpoints_api_discovery_apis(host):
790 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
791 the APIs exposed by a host.
792
793 https://developers.google.com/discovery/v1/reference/apis/list
794 """
maruel380e3262016-08-31 16:10:06 -0700795 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
796 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700797 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
798 if data is None:
799 raise APIError('Failed to discover APIs on %s' % host)
800 out = {}
801 for api in data['items']:
802 if api['id'] == 'discovery:v1':
803 continue
804 # URL is of the following form:
805 # url = host + (
806 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
807 api_data = net.url_read_json(api['discoveryRestUrl'])
808 if api_data is None:
809 raise APIError('Failed to discover %s on %s' % (api['id'], host))
810 out[api['id']] = api_data
811 return out
812
813
maruel0165e822017-06-08 06:26:53 -0700814def get_yielder(base_url, limit):
815 """Returns the first query and a function that yields following items."""
816 CHUNK_SIZE = 250
817
818 url = base_url
819 if limit:
820 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
821 data = net.url_read_json(url)
822 if data is None:
823 # TODO(maruel): Do basic diagnostic.
824 raise Failure('Failed to access %s' % url)
825 org_cursor = data.pop('cursor', None)
826 org_total = len(data.get('items') or [])
827 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
828 if not org_cursor or not org_total:
829 # This is not an iterable resource.
830 return data, lambda: []
831
832 def yielder():
833 cursor = org_cursor
834 total = org_total
835 # Some items support cursors. Try to get automatically if cursors are needed
836 # by looking at the 'cursor' items.
837 while cursor and (not limit or total < limit):
838 merge_char = '&' if '?' in base_url else '?'
839 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
840 if limit:
841 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
842 new = net.url_read_json(url)
843 if new is None:
844 raise Failure('Failed to access %s' % url)
845 cursor = new.get('cursor')
846 new_items = new.get('items')
847 nb_items = len(new_items or [])
848 total += nb_items
849 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
850 yield new_items
851
852 return data, yielder
853
854
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500855### Commands.
856
857
858def abort_task(_swarming, _manifest):
859 """Given a task manifest that was triggered, aborts its execution."""
860 # TODO(vadimsh): No supported by the server yet.
861
862
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400863def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800864 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500865 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500866 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500867 dest='dimensions', metavar='FOO bar',
868 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500869 parser.add_option_group(parser.filter_group)
870
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400871
maruel0165e822017-06-08 06:26:53 -0700872def process_filter_options(parser, options):
873 for key, value in options.dimensions:
874 if ':' in key:
875 parser.error('--dimension key cannot contain ":"')
876 if key.strip() != key:
877 parser.error('--dimension key has whitespace')
878 if not key:
879 parser.error('--dimension key is empty')
880
881 if value.strip() != value:
882 parser.error('--dimension value has whitespace')
883 if not value:
884 parser.error('--dimension value is empty')
885 options.dimensions.sort()
886
887
Vadim Shtayurab450c602014-05-12 19:23:25 -0700888def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400889 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700890 parser.sharding_group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700891 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700892 help='Number of shards to trigger and collect.')
893 parser.add_option_group(parser.sharding_group)
894
895
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400896def add_trigger_options(parser):
897 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500898 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400899 add_filter_options(parser)
900
maruel681d6802017-01-17 16:56:03 -0800901 group = optparse.OptionGroup(parser, 'Task properties')
902 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700903 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500904 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800905 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500906 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700907 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800908 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400909 '--idempotent', action='store_true', default=False,
910 help='When set, the server will actively try to find a previous task '
911 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800912 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700913 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700914 help='The optional path to a file containing the secret_bytes to use with'
915 'this task.')
maruel681d6802017-01-17 16:56:03 -0800916 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700917 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400918 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800919 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700920 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400921 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800922 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500923 '--raw-cmd', action='store_true', default=False,
924 help='When set, the command after -- is used as-is without run_isolated. '
maruela9fe2cb2017-05-10 10:43:23 -0700925 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800926 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700927 '--cipd-package', action='append', default=[], metavar='PKG',
928 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700929 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800930 group.add_option(
931 '--named-cache', action='append', nargs=2, default=[],
maruel3773d8c2017-05-31 15:35:47 -0700932 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800933 help='"<name> <relpath>" items to keep a persistent bot managed cache')
934 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700935 '--service-account',
936 help='Name of a service account to run the task as. Only literal "bot" '
937 'string can be specified currently (to run the task under bot\'s '
938 'account). Don\'t use task service accounts if not given '
939 '(default).')
maruel681d6802017-01-17 16:56:03 -0800940 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700941 '-o', '--output', action='append', default=[], metavar='PATH',
942 help='A list of files to return in addition to those written to '
943 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
944 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -0800945 parser.add_option_group(group)
946
947 group = optparse.OptionGroup(parser, 'Task request')
948 group.add_option(
949 '--priority', type='int', default=100,
950 help='The lower value, the more important the task is')
951 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700952 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -0800953 help='Display name of the task. Defaults to '
954 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
955 'isolated file is provided, if a hash is provided, it defaults to '
956 '<user>/<dimensions>/<isolated hash>/<timestamp>')
957 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700958 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -0800959 help='Tags to assign to the task.')
960 group.add_option(
961 '--user', default='',
962 help='User associated with the task. Defaults to authenticated user on '
963 'the server.')
964 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700965 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -0800966 help='Seconds to allow the task to be pending for a bot to run before '
967 'this task request expires.')
968 group.add_option(
969 '--deadline', type='int', dest='expiration',
970 help=optparse.SUPPRESS_HELP)
971 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000972
973
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500974def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700975 """Processes trigger options and does preparatory steps.
976
maruel4e901792017-05-09 12:07:02 -0700977 Generates service account tokens if necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500978 """
maruel0165e822017-06-08 06:26:53 -0700979 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500980 options.env = dict(options.env)
maruela9fe2cb2017-05-10 10:43:23 -0700981 if args and args[0] == '--':
982 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500983
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500984 if not options.dimensions:
985 parser.error('Please at least specify one --dimension')
maruela9fe2cb2017-05-10 10:43:23 -0700986 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
987 parser.error('--tags must be in the format key:value')
988 if options.raw_cmd and not args:
989 parser.error(
990 'Arguments with --raw-cmd should be passed after -- as command '
991 'delimiter.')
992 if options.isolate_server and not options.namespace:
993 parser.error(
994 '--namespace must be a valid value when --isolate-server is used')
995 if not options.isolated and not options.raw_cmd:
996 parser.error('Specify at least one of --raw-cmd or --isolated or both')
997
998 # Isolated
999 # --isolated is required only if --raw-cmd wasn't provided.
1000 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1001 # preferred server.
1002 isolateserver.process_isolate_server_options(
1003 parser, options, False, not options.raw_cmd)
1004 inputs_ref = None
1005 if options.isolate_server:
1006 inputs_ref = FilesRef(
1007 isolated=options.isolated,
1008 isolatedserver=options.isolate_server,
1009 namespace=options.namespace)
1010
1011 # Command
1012 command = None
1013 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001014 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001015 command = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001016 else:
maruela9fe2cb2017-05-10 10:43:23 -07001017 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001018
maruela9fe2cb2017-05-10 10:43:23 -07001019 # CIPD
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
maruela9fe2cb2017-05-10 10:43:23 -07001036 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001037 secret_bytes = None
1038 if options.secret_bytes_path:
1039 with open(options.secret_bytes_path, 'r') as f:
1040 secret_bytes = f.read().encode('base64')
1041
maruela9fe2cb2017-05-10 10:43:23 -07001042 # Named caches
maruel681d6802017-01-17 16:56:03 -08001043 caches = [
1044 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1045 for i in options.named_cache
1046 ]
maruela9fe2cb2017-05-10 10:43:23 -07001047
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,
maruela9fe2cb2017-05-10 10:43:23 -07001051 command=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,
maruela9fe2cb2017-05-10 10:43:23 -07001055 extra_args=extra_args,
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)
vadimsh93d167c2016-09-13 11:31:51 -07001062
1063 # Convert a service account email to a signed service account token to pass
1064 # to Swarming.
1065 service_account_token = None
1066 if options.service_account in ('bot', 'none'):
1067 service_account_token = options.service_account
1068 elif options.service_account:
1069 # pylint: disable=assignment-from-no-return
1070 service_account_token = mint_service_account_token(options.service_account)
1071
maruel77f720b2015-09-15 12:35:22 -07001072 return NewTaskRequest(
1073 expiration_secs=options.expiration,
maruela9fe2cb2017-05-10 10:43:23 -07001074 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001075 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001077 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001078 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001079 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001080 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001081
1082
1083def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001084 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001085 '-t', '--timeout', type='float',
1086 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1087 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001088 parser.group_logging.add_option(
1089 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001090 parser.group_logging.add_option(
1091 '--print-status-updates', action='store_true',
1092 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001093 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001094 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001095 '--task-summary-json',
1096 metavar='FILE',
1097 help='Dump a summary of task results to this file as json. It contains '
1098 'only shards statuses as know to server directly. Any output files '
1099 'emitted by the task can be collected by using --task-output-dir')
1100 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001101 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001102 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001103 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001104 'directory contains per-shard directory with output files produced '
1105 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001106 parser.task_output_group.add_option(
1107 '--perf', action='store_true', default=False,
1108 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001109 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001110
1111
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001112@subcommand.usage('bots...')
1113def CMDbot_delete(parser, args):
1114 """Forcibly deletes bots from the Swarming server."""
1115 parser.add_option(
1116 '-f', '--force', action='store_true',
1117 help='Do not prompt for confirmation')
1118 options, args = parser.parse_args(args)
1119 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001120 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001121
1122 bots = sorted(args)
1123 if not options.force:
1124 print('Delete the following bots?')
1125 for bot in bots:
1126 print(' %s' % bot)
1127 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1128 print('Goodbye.')
1129 return 1
1130
1131 result = 0
1132 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001133 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001134 if net.url_read_json(url, data={}, method='POST') is None:
1135 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001136 result = 1
1137 return result
1138
1139
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001140def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001141 """Returns information about the bots connected to the Swarming server."""
1142 add_filter_options(parser)
1143 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001144 '--dead-only', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001145 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001146 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001147 '-k', '--keep-dead', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001148 help='Keep both dead and alive bots')
1149 parser.filter_group.add_option(
1150 '--busy', action='store_true', help='Keep only busy bots')
1151 parser.filter_group.add_option(
1152 '--idle', action='store_true', help='Keep only idle bots')
1153 parser.filter_group.add_option(
1154 '--mp', action='store_true',
1155 help='Keep only Machine Provider managed bots')
1156 parser.filter_group.add_option(
1157 '--non-mp', action='store_true',
1158 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001159 parser.filter_group.add_option(
1160 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001161 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001162 options, args = parser.parse_args(args)
maruel0165e822017-06-08 06:26:53 -07001163 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001164
1165 if options.keep_dead and options.dead_only:
maruel0165e822017-06-08 06:26:53 -07001166 parser.error('Use only one of --keep-dead or --dead-only')
1167 if options.busy and options.idle:
1168 parser.error('Use only one of --busy or --idle')
1169 if options.mp and options.non_mp:
1170 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001171
maruel0165e822017-06-08 06:26:53 -07001172 url = options.swarming + '/api/swarming/v1/bots/list?'
1173 values = []
1174 if options.dead_only:
1175 values.append(('is_dead', 'TRUE'))
1176 elif options.keep_dead:
1177 values.append(('is_dead', 'NONE'))
1178 else:
1179 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001180
maruel0165e822017-06-08 06:26:53 -07001181 if options.busy:
1182 values.append(('is_busy', 'TRUE'))
1183 elif options.idle:
1184 values.append(('is_busy', 'FALSE'))
1185 else:
1186 values.append(('is_busy', 'NONE'))
1187
1188 if options.mp:
1189 values.append(('is_mp', 'TRUE'))
1190 elif options.non_mp:
1191 values.append(('is_mp', 'FALSE'))
1192 else:
1193 values.append(('is_mp', 'NONE'))
1194
1195 for key, value in options.dimensions:
1196 values.append(('dimensions', '%s:%s' % (key, value)))
1197 url += urllib.urlencode(values)
1198 try:
1199 data, yielder = get_yielder(url, 0)
1200 bots = data.get('items') or []
1201 for items in yielder():
1202 if items:
1203 bots.extend(items)
1204 except Failure as e:
1205 sys.stderr.write('\n%s\n' % e)
1206 return 1
maruel77f720b2015-09-15 12:35:22 -07001207 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruel0165e822017-06-08 06:26:53 -07001208 print bot['bot_id']
1209 if not options.bare:
1210 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1211 print ' %s' % json.dumps(dimensions, sort_keys=True)
1212 if bot.get('task_id'):
1213 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001214 return 0
1215
1216
maruelfd0a90c2016-06-10 11:51:10 -07001217@subcommand.usage('task_id')
1218def CMDcancel(parser, args):
1219 """Cancels a task."""
1220 options, args = parser.parse_args(args)
1221 if not args:
1222 parser.error('Please specify the task to cancel')
1223 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001224 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001225 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1226 print('Deleting %s failed. Probably already gone' % task_id)
1227 return 1
1228 return 0
1229
1230
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001231@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001232def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001233 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001234
1235 The result can be in multiple part if the execution was sharded. It can
1236 potentially have retries.
1237 """
1238 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001239 parser.add_option(
1240 '-j', '--json',
1241 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001242 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001243 if not args and not options.json:
1244 parser.error('Must specify at least one task id or --json.')
1245 if args and options.json:
1246 parser.error('Only use one of task id or --json.')
1247
1248 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001249 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001250 try:
maruel1ceb3872015-10-14 06:10:44 -07001251 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001252 data = json.load(f)
1253 except (IOError, ValueError):
1254 parser.error('Failed to open %s' % options.json)
1255 try:
1256 tasks = sorted(
1257 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1258 args = [t['task_id'] for t in tasks]
1259 except (KeyError, TypeError):
1260 parser.error('Failed to process %s' % options.json)
1261 if options.timeout is None:
1262 options.timeout = (
1263 data['request']['properties']['execution_timeout_secs'] +
1264 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001265 else:
1266 valid = frozenset('0123456789abcdef')
1267 if any(not valid.issuperset(task_id) for task_id in args):
1268 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001269
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001270 try:
1271 return collect(
1272 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001273 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001274 options.timeout,
1275 options.decorate,
1276 options.print_status_updates,
1277 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001278 options.task_output_dir,
1279 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001280 except Failure:
1281 on_error.report(None)
1282 return 1
1283
1284
maruelbea00862015-09-18 09:55:36 -07001285@subcommand.usage('[filename]')
1286def CMDput_bootstrap(parser, args):
1287 """Uploads a new version of bootstrap.py."""
1288 options, args = parser.parse_args(args)
1289 if len(args) != 1:
1290 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001291 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001292 path = unicode(os.path.abspath(args[0]))
1293 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001294 content = f.read().decode('utf-8')
1295 data = net.url_read_json(url, data={'content': content})
1296 print data
1297 return 0
1298
1299
1300@subcommand.usage('[filename]')
1301def CMDput_bot_config(parser, args):
1302 """Uploads a new version of bot_config.py."""
1303 options, args = parser.parse_args(args)
1304 if len(args) != 1:
1305 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001306 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001307 path = unicode(os.path.abspath(args[0]))
1308 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001309 content = f.read().decode('utf-8')
1310 data = net.url_read_json(url, data={'content': content})
1311 print data
1312 return 0
1313
1314
maruel77f720b2015-09-15 12:35:22 -07001315@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001316def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001317 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1318 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001319
1320 Examples:
maruel0165e822017-06-08 06:26:53 -07001321 Raw task request and results:
1322 swarming.py query -S server-url.com task/123456/request
1323 swarming.py query -S server-url.com task/123456/result
1324
maruel77f720b2015-09-15 12:35:22 -07001325 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001326 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001327
maruel0165e822017-06-08 06:26:53 -07001328 Listing last 10 tasks on a specific bot named 'bot1':
1329 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001330
maruel0165e822017-06-08 06:26:53 -07001331 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001332 quoting is important!:
1333 swarming.py query -S server-url.com --limit 10 \\
maruel0165e822017-06-08 06:26:53 -07001334 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001335 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001336 parser.add_option(
1337 '-L', '--limit', type='int', default=200,
1338 help='Limit to enforce on limitless items (like number of tasks); '
1339 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001340 parser.add_option(
1341 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001342 parser.add_option(
1343 '--progress', action='store_true',
1344 help='Prints a dot at each request to show progress')
1345 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001346 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001347 parser.error(
1348 'Must specify only method name and optionally query args properly '
1349 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001350 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruel0165e822017-06-08 06:26:53 -07001351 try:
1352 data, yielder = get_yielder(base_url, options.limit)
1353 for items in yielder():
1354 if items:
1355 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001356 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001357 sys.stderr.write('.')
1358 sys.stderr.flush()
1359 except Failure as e:
1360 sys.stderr.write('\n%s\n' % e)
1361 return 1
maruel77f720b2015-09-15 12:35:22 -07001362 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001363 sys.stderr.write('\n')
1364 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001365 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001366 options.json = unicode(os.path.abspath(options.json))
1367 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001368 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001369 try:
maruel77f720b2015-09-15 12:35:22 -07001370 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001371 sys.stdout.write('\n')
1372 except IOError:
1373 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001374 return 0
1375
1376
maruel77f720b2015-09-15 12:35:22 -07001377def CMDquery_list(parser, args):
1378 """Returns list of all the Swarming APIs that can be used with command
1379 'query'.
1380 """
1381 parser.add_option(
1382 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1383 options, args = parser.parse_args(args)
1384 if args:
1385 parser.error('No argument allowed.')
1386
1387 try:
1388 apis = endpoints_api_discovery_apis(options.swarming)
1389 except APIError as e:
1390 parser.error(str(e))
1391 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001392 options.json = unicode(os.path.abspath(options.json))
1393 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001394 json.dump(apis, f)
1395 else:
1396 help_url = (
1397 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1398 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001399 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1400 if i:
1401 print('')
maruel77f720b2015-09-15 12:35:22 -07001402 print api_id
maruel11e31af2017-02-15 07:30:50 -08001403 print ' ' + api['description'].strip()
1404 if 'resources' in api:
1405 # Old.
1406 for j, (resource_name, resource) in enumerate(
1407 sorted(api['resources'].iteritems())):
1408 if j:
1409 print('')
1410 for method_name, method in sorted(resource['methods'].iteritems()):
1411 # Only list the GET ones.
1412 if method['httpMethod'] != 'GET':
1413 continue
1414 print '- %s.%s: %s' % (
1415 resource_name, method_name, method['path'])
1416 print('\n'.join(
1417 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1418 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1419 else:
1420 # New.
1421 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001422 # Only list the GET ones.
1423 if method['httpMethod'] != 'GET':
1424 continue
maruel11e31af2017-02-15 07:30:50 -08001425 print '- %s: %s' % (method['id'], method['path'])
1426 print('\n'.join(
1427 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001428 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1429 return 0
1430
1431
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001432@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001433def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001434 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001435
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001436 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001437 """
1438 add_trigger_options(parser)
1439 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001440 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001441 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001442 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001443 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001444 tasks = trigger_task_shards(
1445 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001446 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001447 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001448 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001449 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001450 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001451 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001452 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001453 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001454 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001455 task_ids = [
1456 t['task_id']
1457 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1458 ]
maruel71c61c82016-02-22 06:52:05 -08001459 if options.timeout is None:
1460 options.timeout = (
1461 task_request.properties.execution_timeout_secs +
1462 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001463 try:
1464 return collect(
1465 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001466 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001467 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001468 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001469 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001470 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001471 options.task_output_dir,
1472 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001473 except Failure:
1474 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001475 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001476
1477
maruel18122c62015-10-23 06:31:23 -07001478@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001479def CMDreproduce(parser, args):
1480 """Runs a task locally that was triggered on the server.
1481
1482 This running locally the same commands that have been run on the bot. The data
1483 downloaded will be in a subdirectory named 'work' of the current working
1484 directory.
maruel18122c62015-10-23 06:31:23 -07001485
1486 You can pass further additional arguments to the target command by passing
1487 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001488 """
maruelc070e672016-02-22 17:32:57 -08001489 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001490 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001491 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001492 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001493 extra_args = []
1494 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001495 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001496 if len(args) > 1:
1497 if args[1] == '--':
1498 if len(args) > 2:
1499 extra_args = args[2:]
1500 else:
1501 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001502
maruel380e3262016-08-31 16:10:06 -07001503 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001504 request = net.url_read_json(url)
1505 if not request:
1506 print >> sys.stderr, 'Failed to retrieve request data for the task'
1507 return 1
1508
maruel12e30012015-10-09 11:55:35 -07001509 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001510 if fs.isdir(workdir):
1511 parser.error('Please delete the directory \'work\' first')
1512 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001513 cachedir = unicode(os.path.abspath('cipd_cache'))
1514 if not fs.exists(cachedir):
1515 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001516
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001517 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001518 env = os.environ.copy()
1519 env['SWARMING_BOT_ID'] = 'reproduce'
1520 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001521 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001522 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001523 for i in properties['env']:
1524 key = i['key'].encode('utf-8')
1525 if not i['value']:
1526 env.pop(key, None)
1527 else:
1528 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001529
iannucci31ab9192017-05-02 19:11:56 -07001530 command = []
nodir152cba62016-05-12 16:08:56 -07001531 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001532 # Create the tree.
1533 with isolateserver.get_storage(
1534 properties['inputs_ref']['isolatedserver'],
1535 properties['inputs_ref']['namespace']) as storage:
1536 bundle = isolateserver.fetch_isolated(
1537 properties['inputs_ref']['isolated'],
1538 storage,
1539 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001540 workdir,
1541 False)
maruel29ab2fd2015-10-16 11:44:01 -07001542 command = bundle.command
1543 if bundle.relative_cwd:
1544 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001545 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001546
1547 if properties.get('command'):
1548 command.extend(properties['command'])
1549
1550 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1551 new_command = tools.fix_python_path(command)
1552 new_command = run_isolated.process_command(
1553 new_command, options.output_dir, None)
1554 if not options.output_dir and new_command != command:
1555 parser.error('The task has outputs, you must use --output-dir')
1556 command = new_command
1557 file_path.ensure_command_has_abs_path(command, workdir)
1558
1559 if properties.get('cipd_input'):
1560 ci = properties['cipd_input']
1561 cp = ci['client_package']
1562 client_manager = cipd.get_client(
1563 ci['server'], cp['package_name'], cp['version'], cachedir)
1564
1565 with client_manager as client:
1566 by_path = collections.defaultdict(list)
1567 for pkg in ci['packages']:
1568 path = pkg['path']
1569 # cipd deals with 'root' as ''
1570 if path == '.':
1571 path = ''
1572 by_path[path].append((pkg['package_name'], pkg['version']))
1573 client.ensure(workdir, by_path, cache_dir=cachedir)
1574
maruel77f720b2015-09-15 12:35:22 -07001575 try:
maruel18122c62015-10-23 06:31:23 -07001576 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001577 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001578 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001579 print >> sys.stderr, str(e)
1580 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001581
1582
maruel0eb1d1b2015-10-02 14:48:21 -07001583@subcommand.usage('bot_id')
1584def CMDterminate(parser, args):
1585 """Tells a bot to gracefully shut itself down as soon as it can.
1586
1587 This is done by completing whatever current task there is then exiting the bot
1588 process.
1589 """
1590 parser.add_option(
1591 '--wait', action='store_true', help='Wait for the bot to terminate')
1592 options, args = parser.parse_args(args)
1593 if len(args) != 1:
1594 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001595 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001596 request = net.url_read_json(url, data={})
1597 if not request:
1598 print >> sys.stderr, 'Failed to ask for termination'
1599 return 1
1600 if options.wait:
1601 return collect(
maruel9531ce02016-04-13 06:11:23 -07001602 options.swarming, [request['task_id']], 0., False, False, None, None,
1603 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001604 return 0
1605
1606
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001607@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001608def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001609 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001610
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001611 Passes all extra arguments provided after '--' as additional command line
1612 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001613 """
1614 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001615 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001616 parser.add_option(
1617 '--dump-json',
1618 metavar='FILE',
1619 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001620 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001621 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001622 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001623 tasks = trigger_task_shards(
1624 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001625 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001626 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001627 tasks_sorted = sorted(
1628 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001629 if options.dump_json:
1630 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001631 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001632 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001633 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001634 }
maruel46b015f2015-10-13 18:40:35 -07001635 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001636 print('To collect results, use:')
1637 print(' swarming.py collect -S %s --json %s' %
1638 (options.swarming, options.dump_json))
1639 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001640 print('To collect results, use:')
1641 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001642 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1643 print('Or visit:')
1644 for t in tasks_sorted:
1645 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001646 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001647 except Failure:
1648 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001649 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001650
1651
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001652class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001653 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001654 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001655 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001656 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001657 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001658 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001659 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001660 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001661 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001662 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001663
1664 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001665 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001666 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001667 auth.process_auth_options(self, options)
1668 user = self._process_swarming(options)
1669 if hasattr(options, 'user') and not options.user:
1670 options.user = user
1671 return options, args
1672
1673 def _process_swarming(self, options):
1674 """Processes the --swarming option and aborts if not specified.
1675
1676 Returns the identity as determined by the server.
1677 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001678 if not options.swarming:
1679 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001680 try:
1681 options.swarming = net.fix_url(options.swarming)
1682 except ValueError as e:
1683 self.error('--swarming %s' % e)
1684 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001685 try:
1686 user = auth.ensure_logged_in(options.swarming)
1687 except ValueError as e:
1688 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001689 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001690
1691
1692def main(args):
1693 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001694 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001695
1696
1697if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001698 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001699 fix_encoding.fix_encoding()
1700 tools.disable_buffering()
1701 colorama.init()
1702 sys.exit(main(sys.argv[1:]))