blob: 5c9ad690406534abc63a4cb90e8be9751d00eea3 [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
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07008__version__ = '0.9.2'
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
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
maruelc070e672016-02-22 17:32:57 -080040import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000041
42
tansella4949442016-06-23 22:34:32 -070043ROOT_DIR = os.path.dirname(os.path.abspath(
44 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050045
46
47class Failure(Exception):
48 """Generic failure."""
49 pass
50
51
maruela9fe2cb2017-05-10 10:43:23 -070052def default_task_name(options):
53 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050054 if not options.task_name:
maruela9fe2cb2017-05-10 10:43:23 -070055 task_name = u'%s/%s' % (
maruel4e901792017-05-09 12:07:02 -070056 options.user,
maruel0165e822017-06-08 06:26:53 -070057 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruela9fe2cb2017-05-10 10:43:23 -070058 if options.isolated:
59 task_name += u'/' + options.isolated
60 return task_name
61 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050062
63
64### Triggering.
65
66
maruel77f720b2015-09-15 12:35:22 -070067# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070068CipdPackage = collections.namedtuple(
69 'CipdPackage',
70 [
71 'package_name',
72 'path',
73 'version',
74 ])
75
76
77# See ../appengine/swarming/swarming_rpcs.py.
78CipdInput = collections.namedtuple(
79 'CipdInput',
80 [
81 'client_package',
82 'packages',
83 'server',
84 ])
85
86
87# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070088FilesRef = collections.namedtuple(
89 'FilesRef',
90 [
91 'isolated',
92 'isolatedserver',
93 'namespace',
94 ])
95
96
97# See ../appengine/swarming/swarming_rpcs.py.
98TaskProperties = collections.namedtuple(
99 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500100 [
maruel681d6802017-01-17 16:56:03 -0800101 'caches',
borenet02f772b2016-06-22 12:42:19 -0700102 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500103 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500104 'dimensions',
105 'env',
maruel77f720b2015-09-15 12:35:22 -0700106 'execution_timeout_secs',
107 'extra_args',
108 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500109 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700110 'inputs_ref',
111 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700112 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700113 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700114 ])
115
116
117# See ../appengine/swarming/swarming_rpcs.py.
118NewTaskRequest = collections.namedtuple(
119 'NewTaskRequest',
120 [
121 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500122 'name',
maruel77f720b2015-09-15 12:35:22 -0700123 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500124 'priority',
maruel77f720b2015-09-15 12:35:22 -0700125 'properties',
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700126 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500127 'tags',
128 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500129 ])
130
131
maruel77f720b2015-09-15 12:35:22 -0700132def namedtuple_to_dict(value):
133 """Recursively converts a namedtuple to a dict."""
134 out = dict(value._asdict())
135 for k, v in out.iteritems():
136 if hasattr(v, '_asdict'):
137 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700138 elif isinstance(v, (list, tuple)):
139 l = []
140 for elem in v:
141 if hasattr(elem, '_asdict'):
142 l.append(namedtuple_to_dict(elem))
143 else:
144 l.append(elem)
145 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700146 return out
147
148
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700149def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800150 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700151
152 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500153 """
maruel77f720b2015-09-15 12:35:22 -0700154 out = namedtuple_to_dict(task_request)
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700155 # Don't send 'service_account' if it is None to avoid confusing older
156 # version of the server that doesn't know about 'service_account' and don't
157 # use it at all.
158 if not out['service_account']:
159 out.pop('service_account')
maruel77f720b2015-09-15 12:35:22 -0700160 out['properties']['dimensions'] = [
161 {'key': k, 'value': v}
maruel0165e822017-06-08 06:26:53 -0700162 for k, v in out['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700163 ]
maruel77f720b2015-09-15 12:35:22 -0700164 out['properties']['env'] = [
165 {'key': k, 'value': v}
166 for k, v in out['properties']['env'].iteritems()
167 ]
168 out['properties']['env'].sort(key=lambda x: x['key'])
169 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500170
171
maruel77f720b2015-09-15 12:35:22 -0700172def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500173 """Triggers a request on the Swarming server and returns the json data.
174
175 It's the low-level function.
176
177 Returns:
178 {
179 'request': {
180 'created_ts': u'2010-01-02 03:04:05',
181 'name': ..
182 },
183 'task_id': '12300',
184 }
185 """
186 logging.info('Triggering: %s', raw_request['name'])
187
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500188 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700189 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500190 if not result:
191 on_error.report('Failed to trigger task %s' % raw_request['name'])
192 return None
maruele557bce2015-11-17 09:01:27 -0800193 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800194 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800195 msg = 'Failed to trigger task %s' % raw_request['name']
196 if result['error'].get('errors'):
197 for err in result['error']['errors']:
198 if err.get('message'):
199 msg += '\nMessage: %s' % err['message']
200 if err.get('debugInfo'):
201 msg += '\nDebug info:\n%s' % err['debugInfo']
202 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800203 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800204
205 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800206 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500207 return result
208
209
210def setup_googletest(env, shards, index):
211 """Sets googletest specific environment variables."""
212 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700213 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
214 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
215 env = env[:]
216 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
217 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500218 return env
219
220
221def trigger_task_shards(swarming, task_request, shards):
222 """Triggers one or many subtasks of a sharded task.
223
224 Returns:
225 Dict with task details, returned to caller as part of --dump-json output.
226 None in case of failure.
227 """
228 def convert(index):
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700229 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500230 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700231 req['properties']['env'] = setup_googletest(
232 req['properties']['env'], shards, index)
233 req['name'] += ':%s:%s' % (index, shards)
234 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500235
236 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500237 tasks = {}
238 priority_warning = False
239 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700240 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500241 if not task:
242 break
243 logging.info('Request result: %s', task)
244 if (not priority_warning and
Marc-Antoine Ruel49ea2182017-08-17 10:07:49 -0400245 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500246 priority_warning = True
247 print >> sys.stderr, (
248 'Priority was reset to %s' % task['request']['priority'])
249 tasks[request['name']] = {
250 'shard_index': index,
251 'task_id': task['task_id'],
252 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
253 }
254
255 # Some shards weren't triggered. Abort everything.
256 if len(tasks) != len(requests):
257 if tasks:
258 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
259 len(tasks), len(requests))
260 for task_dict in tasks.itervalues():
261 abort_task(swarming, task_dict['task_id'])
262 return None
263
264 return tasks
265
266
267### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000268
269
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700270# How often to print status updates to stdout in 'collect'.
271STATUS_UPDATE_INTERVAL = 15 * 60.
272
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400273
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400274class State(object):
275 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000276
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400277 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
278 values are part of the API so if they change, the API changed.
279
280 It's in fact an enum. Values should be in decreasing order of importance.
281 """
282 RUNNING = 0x10
283 PENDING = 0x20
284 EXPIRED = 0x30
285 TIMED_OUT = 0x40
286 BOT_DIED = 0x50
287 CANCELED = 0x60
288 COMPLETED = 0x70
289
maruel77f720b2015-09-15 12:35:22 -0700290 STATES = (
291 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
292 'COMPLETED')
293 STATES_RUNNING = ('RUNNING', 'PENDING')
294 STATES_NOT_RUNNING = (
295 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
296 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
297 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400298
299 _NAMES = {
300 RUNNING: 'Running',
301 PENDING: 'Pending',
302 EXPIRED: 'Expired',
303 TIMED_OUT: 'Execution timed out',
304 BOT_DIED: 'Bot died',
305 CANCELED: 'User canceled',
306 COMPLETED: 'Completed',
307 }
308
maruel77f720b2015-09-15 12:35:22 -0700309 _ENUMS = {
310 'RUNNING': RUNNING,
311 'PENDING': PENDING,
312 'EXPIRED': EXPIRED,
313 'TIMED_OUT': TIMED_OUT,
314 'BOT_DIED': BOT_DIED,
315 'CANCELED': CANCELED,
316 'COMPLETED': COMPLETED,
317 }
318
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400319 @classmethod
320 def to_string(cls, state):
321 """Returns a user-readable string representing a State."""
322 if state not in cls._NAMES:
323 raise ValueError('Invalid state %s' % state)
324 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000325
maruel77f720b2015-09-15 12:35:22 -0700326 @classmethod
327 def from_enum(cls, state):
328 """Returns int value based on the string."""
329 if state not in cls._ENUMS:
330 raise ValueError('Invalid state %s' % state)
331 return cls._ENUMS[state]
332
maruel@chromium.org0437a732013-08-27 16:05:52 +0000333
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700334class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700335 """Assembles task execution summary (for --task-summary-json output).
336
337 Optionally fetches task outputs from isolate server to local disk (used when
338 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700339
340 This object is shared among multiple threads running 'retrieve_results'
341 function, in particular they call 'process_shard_result' method in parallel.
342 """
343
maruel0eb1d1b2015-10-02 14:48:21 -0700344 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700345 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
346
347 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700348 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700349 shard_count: expected number of task shards.
350 """
maruel12e30012015-10-09 11:55:35 -0700351 self.task_output_dir = (
352 unicode(os.path.abspath(task_output_dir))
353 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700354 self.shard_count = shard_count
355
356 self._lock = threading.Lock()
357 self._per_shard_results = {}
358 self._storage = None
359
nodire5028a92016-04-29 14:38:21 -0700360 if self.task_output_dir:
361 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362
Vadim Shtayurab450c602014-05-12 19:23:25 -0700363 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364 """Stores results of a single task shard, fetches output files if necessary.
365
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400366 Modifies |result| in place.
367
maruel77f720b2015-09-15 12:35:22 -0700368 shard_index is 0-based.
369
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370 Called concurrently from multiple threads.
371 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700373 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374 if shard_index < 0 or shard_index >= self.shard_count:
375 logging.warning(
376 'Shard index %d is outside of expected range: [0; %d]',
377 shard_index, self.shard_count - 1)
378 return
379
maruel77f720b2015-09-15 12:35:22 -0700380 if result.get('outputs_ref'):
381 ref = result['outputs_ref']
382 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
383 ref['isolatedserver'],
384 urllib.urlencode(
385 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400386
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 # Store result dict of that shard, ignore results we've already seen.
388 with self._lock:
389 if shard_index in self._per_shard_results:
390 logging.warning('Ignoring duplicate shard index %d', shard_index)
391 return
392 self._per_shard_results[shard_index] = result
393
394 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700395 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400396 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700397 result['outputs_ref']['isolatedserver'],
398 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400399 if storage:
400 # Output files are supposed to be small and they are not reused across
401 # tasks. So use MemoryCache for them instead of on-disk cache. Make
402 # files writable, so that calling script can delete them.
403 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700404 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400405 storage,
406 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700407 os.path.join(self.task_output_dir, str(shard_index)),
408 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700409
410 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700411 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700412 with self._lock:
413 # Write an array of shard results with None for missing shards.
414 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700415 'shards': [
416 self._per_shard_results.get(i) for i in xrange(self.shard_count)
417 ],
418 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700419 # Write summary.json to task_output_dir as well.
420 if self.task_output_dir:
421 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700422 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700423 summary,
424 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700425 if self._storage:
426 self._storage.close()
427 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700428 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700429
430 def _get_storage(self, isolate_server, namespace):
431 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700432 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 with self._lock:
434 if not self._storage:
435 self._storage = isolateserver.get_storage(isolate_server, namespace)
436 else:
437 # Shards must all use exact same isolate server and namespace.
438 if self._storage.location != isolate_server:
439 logging.error(
440 'Task shards are using multiple isolate servers: %s and %s',
441 self._storage.location, isolate_server)
442 return None
443 if self._storage.namespace != namespace:
444 logging.error(
445 'Task shards are using multiple namespaces: %s and %s',
446 self._storage.namespace, namespace)
447 return None
448 return self._storage
449
450
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500451def now():
452 """Exists so it can be mocked easily."""
453 return time.time()
454
455
maruel77f720b2015-09-15 12:35:22 -0700456def parse_time(value):
457 """Converts serialized time from the API to datetime.datetime."""
458 # When microseconds are 0, the '.123456' suffix is elided. This means the
459 # serialized format is not consistent, which confuses the hell out of python.
460 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
461 try:
462 return datetime.datetime.strptime(value, fmt)
463 except ValueError:
464 pass
465 raise ValueError('Failed to parse %s' % value)
466
467
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700468def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700469 base_url, shard_index, task_id, timeout, should_stop, output_collector,
470 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400471 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700472
Vadim Shtayurab450c602014-05-12 19:23:25 -0700473 Returns:
474 <result dict> on success.
475 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700476 """
maruel71c61c82016-02-22 06:52:05 -0800477 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700478 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700479 if include_perf:
480 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700481 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700482 started = now()
483 deadline = started + timeout if timeout else None
484 attempt = 0
485
486 while not should_stop.is_set():
487 attempt += 1
488
489 # Waiting for too long -> give up.
490 current_time = now()
491 if deadline and current_time >= deadline:
492 logging.error('retrieve_results(%s) timed out on attempt %d',
493 base_url, attempt)
494 return None
495
496 # Do not spin too fast. Spin faster at the beginning though.
497 # Start with 1 sec delay and for each 30 sec of waiting add another second
498 # of delay, until hitting 15 sec ceiling.
499 if attempt > 1:
500 max_delay = min(15, 1 + (current_time - started) / 30.0)
501 delay = min(max_delay, deadline - current_time) if deadline else max_delay
502 if delay > 0:
503 logging.debug('Waiting %.1f sec before retrying', delay)
504 should_stop.wait(delay)
505 if should_stop.is_set():
506 return None
507
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400508 # Disable internal retries in net.url_read_json, since we are doing retries
509 # ourselves.
510 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700511 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
512 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400513 result = net.url_read_json(result_url, retry_50x=False)
514 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400515 continue
maruel77f720b2015-09-15 12:35:22 -0700516
maruelbf53e042015-12-01 15:00:51 -0800517 if result.get('error'):
518 # An error occurred.
519 if result['error'].get('errors'):
520 for err in result['error']['errors']:
521 logging.warning(
522 'Error while reading task: %s; %s',
523 err.get('message'), err.get('debugInfo'))
524 elif result['error'].get('message'):
525 logging.warning(
526 'Error while reading task: %s', result['error']['message'])
527 continue
528
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400529 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700530 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700532 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700533 # Record the result, try to fetch attached output files (if any).
534 if output_collector:
535 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700536 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700537 if result.get('internal_failure'):
538 logging.error('Internal error!')
539 elif result['state'] == 'BOT_DIED':
540 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700541 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000542
543
maruel77f720b2015-09-15 12:35:22 -0700544def convert_to_old_format(result):
545 """Converts the task result data from Endpoints API format to old API format
546 for compatibility.
547
548 This goes into the file generated as --task-summary-json.
549 """
550 # Sets default.
551 result.setdefault('abandoned_ts', None)
552 result.setdefault('bot_id', None)
553 result.setdefault('bot_version', None)
554 result.setdefault('children_task_ids', [])
555 result.setdefault('completed_ts', None)
556 result.setdefault('cost_saved_usd', None)
557 result.setdefault('costs_usd', None)
558 result.setdefault('deduped_from', None)
559 result.setdefault('name', None)
560 result.setdefault('outputs_ref', None)
561 result.setdefault('properties_hash', None)
562 result.setdefault('server_versions', None)
563 result.setdefault('started_ts', None)
564 result.setdefault('tags', None)
565 result.setdefault('user', None)
566
567 # Convertion back to old API.
568 duration = result.pop('duration', None)
569 result['durations'] = [duration] if duration else []
570 exit_code = result.pop('exit_code', None)
571 result['exit_codes'] = [int(exit_code)] if exit_code else []
572 result['id'] = result.pop('task_id')
573 result['isolated_out'] = result.get('outputs_ref', None)
574 output = result.pop('output', None)
575 result['outputs'] = [output] if output else []
576 # properties_hash
577 # server_version
578 # Endpoints result 'state' as string. For compatibility with old code, convert
579 # to int.
580 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700581 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700582 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700583 if 'bot_dimensions' in result:
584 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700585 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700586 }
587 else:
588 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700589
590
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700591def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400592 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700593 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500594 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000595
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700596 Duplicate shards are ignored. Shards are yielded in order of completion.
597 Timed out shards are NOT yielded at all. Caller can compare number of yielded
598 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000599
600 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500601 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 +0000602 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500603
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700604 output_collector is an optional instance of TaskOutputCollector that will be
605 used to fetch files produced by a task from isolate server to the local disk.
606
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500607 Yields:
608 (index, result). In particular, 'result' is defined as the
609 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000610 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000611 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400612 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700613 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700614 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700615
maruel@chromium.org0437a732013-08-27 16:05:52 +0000616 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
617 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700618 # Adds a task to the thread pool to call 'retrieve_results' and return
619 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400620 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700621 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000622 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400623 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700624 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700625
626 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400627 for shard_index, task_id in enumerate(task_ids):
628 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700629
630 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400631 shards_remaining = range(len(task_ids))
632 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700633 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700635 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700636 shard_index, result = results_channel.pull(
637 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700638 except threading_utils.TaskChannel.Timeout:
639 if print_status_updates:
640 print(
641 'Waiting for results from the following shards: %s' %
642 ', '.join(map(str, shards_remaining)))
643 sys.stdout.flush()
644 continue
645 except Exception:
646 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700647
648 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500651 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000652 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654 # Yield back results to the caller.
655 assert shard_index in shards_remaining
656 shards_remaining.remove(shard_index)
657 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700658
maruel@chromium.org0437a732013-08-27 16:05:52 +0000659 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000661 should_stop.set()
662
663
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400664def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000665 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700666 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400667 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700668 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
669 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400670 else:
671 pending = 'N/A'
672
maruel77f720b2015-09-15 12:35:22 -0700673 if metadata.get('duration') is not None:
674 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400675 else:
676 duration = 'N/A'
677
maruel77f720b2015-09-15 12:35:22 -0700678 if metadata.get('exit_code') is not None:
679 # Integers are encoded as string to not loose precision.
680 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400681 else:
682 exit_code = 'N/A'
683
684 bot_id = metadata.get('bot_id') or 'N/A'
685
maruel77f720b2015-09-15 12:35:22 -0700686 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400687 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400688 tag_footer = (
689 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
690 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400691
692 tag_len = max(len(tag_header), len(tag_footer))
693 dash_pad = '+-%s-+\n' % ('-' * tag_len)
694 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
695 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
696
697 header = dash_pad + tag_header + dash_pad
698 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700699 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400700 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000701
702
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700703def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700704 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700705 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700706 """Retrieves results of a Swarming task.
707
708 Returns:
709 process exit code that should be returned to the user.
710 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700711 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700712 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700713
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700714 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700715 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400716 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700717 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400718 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400719 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700720 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700721 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700722
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400723 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700724 shard_exit_code = metadata.get('exit_code')
725 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700726 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700727 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700728 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400729 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700730 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700731
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700732 if decorate:
leileied181762016-10-13 14:24:59 -0700733 s = decorate_shard_output(swarming, index, metadata).encode(
734 'utf-8', 'replace')
735 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400736 if len(seen_shards) < len(task_ids):
737 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700738 else:
maruel77f720b2015-09-15 12:35:22 -0700739 print('%s: %s %s' % (
740 metadata.get('bot_id', 'N/A'),
741 metadata['task_id'],
742 shard_exit_code))
743 if metadata['output']:
744 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400745 if output:
746 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700747 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700748 summary = output_collector.finalize()
749 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700750 # TODO(maruel): Make this optional.
751 for i in summary['shards']:
752 if i:
753 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700754 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700755
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400756 if decorate and total_duration:
757 print('Total duration: %.1fs' % total_duration)
758
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400759 if len(seen_shards) != len(task_ids):
760 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700761 print >> sys.stderr, ('Results from some shards are missing: %s' %
762 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700763 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700764
maruela5490782015-09-30 10:56:59 -0700765 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000766
767
maruel77f720b2015-09-15 12:35:22 -0700768### API management.
769
770
771class APIError(Exception):
772 pass
773
774
775def endpoints_api_discovery_apis(host):
776 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
777 the APIs exposed by a host.
778
779 https://developers.google.com/discovery/v1/reference/apis/list
780 """
maruel380e3262016-08-31 16:10:06 -0700781 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
782 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700783 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
784 if data is None:
785 raise APIError('Failed to discover APIs on %s' % host)
786 out = {}
787 for api in data['items']:
788 if api['id'] == 'discovery:v1':
789 continue
790 # URL is of the following form:
791 # url = host + (
792 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
793 api_data = net.url_read_json(api['discoveryRestUrl'])
794 if api_data is None:
795 raise APIError('Failed to discover %s on %s' % (api['id'], host))
796 out[api['id']] = api_data
797 return out
798
799
maruel0165e822017-06-08 06:26:53 -0700800def get_yielder(base_url, limit):
801 """Returns the first query and a function that yields following items."""
802 CHUNK_SIZE = 250
803
804 url = base_url
805 if limit:
806 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
807 data = net.url_read_json(url)
808 if data is None:
809 # TODO(maruel): Do basic diagnostic.
810 raise Failure('Failed to access %s' % url)
811 org_cursor = data.pop('cursor', None)
812 org_total = len(data.get('items') or [])
813 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
814 if not org_cursor or not org_total:
815 # This is not an iterable resource.
816 return data, lambda: []
817
818 def yielder():
819 cursor = org_cursor
820 total = org_total
821 # Some items support cursors. Try to get automatically if cursors are needed
822 # by looking at the 'cursor' items.
823 while cursor and (not limit or total < limit):
824 merge_char = '&' if '?' in base_url else '?'
825 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
826 if limit:
827 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
828 new = net.url_read_json(url)
829 if new is None:
830 raise Failure('Failed to access %s' % url)
831 cursor = new.get('cursor')
832 new_items = new.get('items')
833 nb_items = len(new_items or [])
834 total += nb_items
835 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
836 yield new_items
837
838 return data, yielder
839
840
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500841### Commands.
842
843
844def abort_task(_swarming, _manifest):
845 """Given a task manifest that was triggered, aborts its execution."""
846 # TODO(vadimsh): No supported by the server yet.
847
848
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400849def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800850 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500851 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500852 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500853 dest='dimensions', metavar='FOO bar',
854 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500855 parser.add_option_group(parser.filter_group)
856
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400857
maruel0165e822017-06-08 06:26:53 -0700858def process_filter_options(parser, options):
859 for key, value in options.dimensions:
860 if ':' in key:
861 parser.error('--dimension key cannot contain ":"')
862 if key.strip() != key:
863 parser.error('--dimension key has whitespace')
864 if not key:
865 parser.error('--dimension key is empty')
866
867 if value.strip() != value:
868 parser.error('--dimension value has whitespace')
869 if not value:
870 parser.error('--dimension value is empty')
871 options.dimensions.sort()
872
873
Vadim Shtayurab450c602014-05-12 19:23:25 -0700874def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400875 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700876 parser.sharding_group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700877 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700878 help='Number of shards to trigger and collect.')
879 parser.add_option_group(parser.sharding_group)
880
881
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400882def add_trigger_options(parser):
883 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500884 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400885 add_filter_options(parser)
886
maruel681d6802017-01-17 16:56:03 -0800887 group = optparse.OptionGroup(parser, 'Task properties')
888 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700889 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500890 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800891 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500892 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700893 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800894 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400895 '--idempotent', action='store_true', default=False,
896 help='When set, the server will actively try to find a previous task '
897 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800898 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700899 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700900 help='The optional path to a file containing the secret_bytes to use with'
901 'this task.')
maruel681d6802017-01-17 16:56:03 -0800902 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700903 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400904 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800905 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700906 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400907 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800908 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500909 '--raw-cmd', action='store_true', default=False,
910 help='When set, the command after -- is used as-is without run_isolated. '
maruela9fe2cb2017-05-10 10:43:23 -0700911 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800912 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700913 '--cipd-package', action='append', default=[], metavar='PKG',
914 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700915 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800916 group.add_option(
917 '--named-cache', action='append', nargs=2, default=[],
maruel3773d8c2017-05-31 15:35:47 -0700918 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800919 help='"<name> <relpath>" items to keep a persistent bot managed cache')
920 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700921 '--service-account',
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700922 help='Email of a service account to run the task as, or literal "bot" '
923 'string to indicate that the task should use the same account the '
924 'bot itself is using to authenticate to Swarming. Don\'t use task '
925 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800926 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700927 '-o', '--output', action='append', default=[], metavar='PATH',
928 help='A list of files to return in addition to those written to '
929 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
930 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -0800931 parser.add_option_group(group)
932
933 group = optparse.OptionGroup(parser, 'Task request')
934 group.add_option(
935 '--priority', type='int', default=100,
936 help='The lower value, the more important the task is')
937 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700938 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -0800939 help='Display name of the task. Defaults to '
940 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
941 'isolated file is provided, if a hash is provided, it defaults to '
942 '<user>/<dimensions>/<isolated hash>/<timestamp>')
943 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700944 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -0800945 help='Tags to assign to the task.')
946 group.add_option(
947 '--user', default='',
948 help='User associated with the task. Defaults to authenticated user on '
949 'the server.')
950 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700951 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -0800952 help='Seconds to allow the task to be pending for a bot to run before '
953 'this task request expires.')
954 group.add_option(
955 '--deadline', type='int', dest='expiration',
956 help=optparse.SUPPRESS_HELP)
957 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000958
959
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500960def process_trigger_options(parser, options, args):
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700961 """Processes trigger options and does preparatory steps."""
maruel0165e822017-06-08 06:26:53 -0700962 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500963 options.env = dict(options.env)
maruela9fe2cb2017-05-10 10:43:23 -0700964 if args and args[0] == '--':
965 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500966
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500967 if not options.dimensions:
968 parser.error('Please at least specify one --dimension')
maruela9fe2cb2017-05-10 10:43:23 -0700969 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
970 parser.error('--tags must be in the format key:value')
971 if options.raw_cmd and not args:
972 parser.error(
973 'Arguments with --raw-cmd should be passed after -- as command '
974 'delimiter.')
975 if options.isolate_server and not options.namespace:
976 parser.error(
977 '--namespace must be a valid value when --isolate-server is used')
978 if not options.isolated and not options.raw_cmd:
979 parser.error('Specify at least one of --raw-cmd or --isolated or both')
980
981 # Isolated
982 # --isolated is required only if --raw-cmd wasn't provided.
983 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
984 # preferred server.
985 isolateserver.process_isolate_server_options(
986 parser, options, False, not options.raw_cmd)
987 inputs_ref = None
988 if options.isolate_server:
989 inputs_ref = FilesRef(
990 isolated=options.isolated,
991 isolatedserver=options.isolate_server,
992 namespace=options.namespace)
993
994 # Command
995 command = None
996 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500997 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500998 command = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500999 else:
maruela9fe2cb2017-05-10 10:43:23 -07001000 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001001
maruela9fe2cb2017-05-10 10:43:23 -07001002 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001003 cipd_packages = []
1004 for p in options.cipd_package:
1005 split = p.split(':', 2)
1006 if len(split) != 3:
1007 parser.error('CIPD packages must take the form: path:package:version')
1008 cipd_packages.append(CipdPackage(
1009 package_name=split[1],
1010 path=split[0],
1011 version=split[2]))
1012 cipd_input = None
1013 if cipd_packages:
1014 cipd_input = CipdInput(
1015 client_package=None,
1016 packages=cipd_packages,
1017 server=None)
1018
maruela9fe2cb2017-05-10 10:43:23 -07001019 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001020 secret_bytes = None
1021 if options.secret_bytes_path:
1022 with open(options.secret_bytes_path, 'r') as f:
1023 secret_bytes = f.read().encode('base64')
1024
maruela9fe2cb2017-05-10 10:43:23 -07001025 # Named caches
maruel681d6802017-01-17 16:56:03 -08001026 caches = [
1027 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1028 for i in options.named_cache
1029 ]
maruela9fe2cb2017-05-10 10:43:23 -07001030
maruel77f720b2015-09-15 12:35:22 -07001031 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001032 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001033 cipd_input=cipd_input,
maruela9fe2cb2017-05-10 10:43:23 -07001034 command=command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001035 dimensions=options.dimensions,
1036 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001037 execution_timeout_secs=options.hard_timeout,
maruela9fe2cb2017-05-10 10:43:23 -07001038 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001039 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001040 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001041 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001042 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001043 outputs=options.output,
1044 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001045
maruel77f720b2015-09-15 12:35:22 -07001046 return NewTaskRequest(
1047 expiration_secs=options.expiration,
maruela9fe2cb2017-05-10 10:43:23 -07001048 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001049 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001050 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001051 properties=properties,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001052 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001053 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001054 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001055
1056
1057def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001058 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001059 '-t', '--timeout', type='float',
1060 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1061 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001062 parser.group_logging.add_option(
1063 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001064 parser.group_logging.add_option(
1065 '--print-status-updates', action='store_true',
1066 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001067 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001068 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001069 '--task-summary-json',
1070 metavar='FILE',
1071 help='Dump a summary of task results to this file as json. It contains '
1072 'only shards statuses as know to server directly. Any output files '
1073 'emitted by the task can be collected by using --task-output-dir')
1074 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001075 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001076 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001077 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001078 'directory contains per-shard directory with output files produced '
1079 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001080 parser.task_output_group.add_option(
1081 '--perf', action='store_true', default=False,
1082 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001083 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001084
1085
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001086@subcommand.usage('bots...')
1087def CMDbot_delete(parser, args):
1088 """Forcibly deletes bots from the Swarming server."""
1089 parser.add_option(
1090 '-f', '--force', action='store_true',
1091 help='Do not prompt for confirmation')
1092 options, args = parser.parse_args(args)
1093 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001094 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001095
1096 bots = sorted(args)
1097 if not options.force:
1098 print('Delete the following bots?')
1099 for bot in bots:
1100 print(' %s' % bot)
1101 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1102 print('Goodbye.')
1103 return 1
1104
1105 result = 0
1106 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001107 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001108 if net.url_read_json(url, data={}, method='POST') is None:
1109 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001110 result = 1
1111 return result
1112
1113
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001114def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001115 """Returns information about the bots connected to the Swarming server."""
1116 add_filter_options(parser)
1117 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001118 '--dead-only', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001119 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001120 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001121 '-k', '--keep-dead', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001122 help='Keep both dead and alive bots')
1123 parser.filter_group.add_option(
1124 '--busy', action='store_true', help='Keep only busy bots')
1125 parser.filter_group.add_option(
1126 '--idle', action='store_true', help='Keep only idle bots')
1127 parser.filter_group.add_option(
1128 '--mp', action='store_true',
1129 help='Keep only Machine Provider managed bots')
1130 parser.filter_group.add_option(
1131 '--non-mp', action='store_true',
1132 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001133 parser.filter_group.add_option(
1134 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001135 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001136 options, args = parser.parse_args(args)
maruel0165e822017-06-08 06:26:53 -07001137 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001138
1139 if options.keep_dead and options.dead_only:
maruel0165e822017-06-08 06:26:53 -07001140 parser.error('Use only one of --keep-dead or --dead-only')
1141 if options.busy and options.idle:
1142 parser.error('Use only one of --busy or --idle')
1143 if options.mp and options.non_mp:
1144 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001145
maruel0165e822017-06-08 06:26:53 -07001146 url = options.swarming + '/api/swarming/v1/bots/list?'
1147 values = []
1148 if options.dead_only:
1149 values.append(('is_dead', 'TRUE'))
1150 elif options.keep_dead:
1151 values.append(('is_dead', 'NONE'))
1152 else:
1153 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001154
maruel0165e822017-06-08 06:26:53 -07001155 if options.busy:
1156 values.append(('is_busy', 'TRUE'))
1157 elif options.idle:
1158 values.append(('is_busy', 'FALSE'))
1159 else:
1160 values.append(('is_busy', 'NONE'))
1161
1162 if options.mp:
1163 values.append(('is_mp', 'TRUE'))
1164 elif options.non_mp:
1165 values.append(('is_mp', 'FALSE'))
1166 else:
1167 values.append(('is_mp', 'NONE'))
1168
1169 for key, value in options.dimensions:
1170 values.append(('dimensions', '%s:%s' % (key, value)))
1171 url += urllib.urlencode(values)
1172 try:
1173 data, yielder = get_yielder(url, 0)
1174 bots = data.get('items') or []
1175 for items in yielder():
1176 if items:
1177 bots.extend(items)
1178 except Failure as e:
1179 sys.stderr.write('\n%s\n' % e)
1180 return 1
maruel77f720b2015-09-15 12:35:22 -07001181 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruel0165e822017-06-08 06:26:53 -07001182 print bot['bot_id']
1183 if not options.bare:
1184 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1185 print ' %s' % json.dumps(dimensions, sort_keys=True)
1186 if bot.get('task_id'):
1187 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001188 return 0
1189
1190
maruelfd0a90c2016-06-10 11:51:10 -07001191@subcommand.usage('task_id')
1192def CMDcancel(parser, args):
1193 """Cancels a task."""
1194 options, args = parser.parse_args(args)
1195 if not args:
1196 parser.error('Please specify the task to cancel')
1197 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001198 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001199 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1200 print('Deleting %s failed. Probably already gone' % task_id)
1201 return 1
1202 return 0
1203
1204
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001205@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001206def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001207 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001208
1209 The result can be in multiple part if the execution was sharded. It can
1210 potentially have retries.
1211 """
1212 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001213 parser.add_option(
1214 '-j', '--json',
1215 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001216 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001217 if not args and not options.json:
1218 parser.error('Must specify at least one task id or --json.')
1219 if args and options.json:
1220 parser.error('Only use one of task id or --json.')
1221
1222 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001223 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001224 try:
maruel1ceb3872015-10-14 06:10:44 -07001225 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001226 data = json.load(f)
1227 except (IOError, ValueError):
1228 parser.error('Failed to open %s' % options.json)
1229 try:
1230 tasks = sorted(
1231 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1232 args = [t['task_id'] for t in tasks]
1233 except (KeyError, TypeError):
1234 parser.error('Failed to process %s' % options.json)
1235 if options.timeout is None:
1236 options.timeout = (
1237 data['request']['properties']['execution_timeout_secs'] +
1238 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001239 else:
1240 valid = frozenset('0123456789abcdef')
1241 if any(not valid.issuperset(task_id) for task_id in args):
1242 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001243
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001244 try:
1245 return collect(
1246 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001247 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001248 options.timeout,
1249 options.decorate,
1250 options.print_status_updates,
1251 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001252 options.task_output_dir,
1253 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001254 except Failure:
1255 on_error.report(None)
1256 return 1
1257
1258
maruelbea00862015-09-18 09:55:36 -07001259@subcommand.usage('[filename]')
1260def CMDput_bootstrap(parser, args):
1261 """Uploads a new version of bootstrap.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_bootstrap'
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
1274@subcommand.usage('[filename]')
1275def CMDput_bot_config(parser, args):
1276 """Uploads a new version of bot_config.py."""
1277 options, args = parser.parse_args(args)
1278 if len(args) != 1:
1279 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001280 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001281 path = unicode(os.path.abspath(args[0]))
1282 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001283 content = f.read().decode('utf-8')
1284 data = net.url_read_json(url, data={'content': content})
1285 print data
1286 return 0
1287
1288
maruel77f720b2015-09-15 12:35:22 -07001289@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001290def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001291 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1292 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001293
1294 Examples:
maruel0165e822017-06-08 06:26:53 -07001295 Raw task request and results:
1296 swarming.py query -S server-url.com task/123456/request
1297 swarming.py query -S server-url.com task/123456/result
1298
maruel77f720b2015-09-15 12:35:22 -07001299 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001300 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001301
maruel0165e822017-06-08 06:26:53 -07001302 Listing last 10 tasks on a specific bot named 'bot1':
1303 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001304
maruel0165e822017-06-08 06:26:53 -07001305 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001306 quoting is important!:
1307 swarming.py query -S server-url.com --limit 10 \\
maruel0165e822017-06-08 06:26:53 -07001308 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001309 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001310 parser.add_option(
1311 '-L', '--limit', type='int', default=200,
1312 help='Limit to enforce on limitless items (like number of tasks); '
1313 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001314 parser.add_option(
1315 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001316 parser.add_option(
1317 '--progress', action='store_true',
1318 help='Prints a dot at each request to show progress')
1319 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001320 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001321 parser.error(
1322 'Must specify only method name and optionally query args properly '
1323 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001324 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruel0165e822017-06-08 06:26:53 -07001325 try:
1326 data, yielder = get_yielder(base_url, options.limit)
1327 for items in yielder():
1328 if items:
1329 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001330 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001331 sys.stderr.write('.')
1332 sys.stderr.flush()
1333 except Failure as e:
1334 sys.stderr.write('\n%s\n' % e)
1335 return 1
maruel77f720b2015-09-15 12:35:22 -07001336 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001337 sys.stderr.write('\n')
1338 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001339 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001340 options.json = unicode(os.path.abspath(options.json))
1341 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001342 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001343 try:
maruel77f720b2015-09-15 12:35:22 -07001344 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001345 sys.stdout.write('\n')
1346 except IOError:
1347 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001348 return 0
1349
1350
maruel77f720b2015-09-15 12:35:22 -07001351def CMDquery_list(parser, args):
1352 """Returns list of all the Swarming APIs that can be used with command
1353 'query'.
1354 """
1355 parser.add_option(
1356 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1357 options, args = parser.parse_args(args)
1358 if args:
1359 parser.error('No argument allowed.')
1360
1361 try:
1362 apis = endpoints_api_discovery_apis(options.swarming)
1363 except APIError as e:
1364 parser.error(str(e))
1365 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001366 options.json = unicode(os.path.abspath(options.json))
1367 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001368 json.dump(apis, f)
1369 else:
1370 help_url = (
1371 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1372 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001373 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1374 if i:
1375 print('')
maruel77f720b2015-09-15 12:35:22 -07001376 print api_id
maruel11e31af2017-02-15 07:30:50 -08001377 print ' ' + api['description'].strip()
1378 if 'resources' in api:
1379 # Old.
1380 for j, (resource_name, resource) in enumerate(
1381 sorted(api['resources'].iteritems())):
1382 if j:
1383 print('')
1384 for method_name, method in sorted(resource['methods'].iteritems()):
1385 # Only list the GET ones.
1386 if method['httpMethod'] != 'GET':
1387 continue
1388 print '- %s.%s: %s' % (
1389 resource_name, method_name, method['path'])
1390 print('\n'.join(
1391 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1392 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1393 else:
1394 # New.
1395 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001396 # Only list the GET ones.
1397 if method['httpMethod'] != 'GET':
1398 continue
maruel11e31af2017-02-15 07:30:50 -08001399 print '- %s: %s' % (method['id'], method['path'])
1400 print('\n'.join(
1401 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001402 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1403 return 0
1404
1405
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001406@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001407def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001408 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001409
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001410 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001411 """
1412 add_trigger_options(parser)
1413 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001414 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001415 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001416 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001417 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001418 tasks = trigger_task_shards(
1419 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001420 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001421 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001422 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001423 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001424 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001425 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001426 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001427 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001428 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001429 task_ids = [
1430 t['task_id']
1431 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1432 ]
maruel71c61c82016-02-22 06:52:05 -08001433 if options.timeout is None:
1434 options.timeout = (
1435 task_request.properties.execution_timeout_secs +
1436 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001437 try:
1438 return collect(
1439 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001440 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001441 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001442 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001443 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001444 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001445 options.task_output_dir,
1446 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001447 except Failure:
1448 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001449 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001450
1451
maruel18122c62015-10-23 06:31:23 -07001452@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001453def CMDreproduce(parser, args):
1454 """Runs a task locally that was triggered on the server.
1455
1456 This running locally the same commands that have been run on the bot. The data
1457 downloaded will be in a subdirectory named 'work' of the current working
1458 directory.
maruel18122c62015-10-23 06:31:23 -07001459
1460 You can pass further additional arguments to the target command by passing
1461 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001462 """
maruelc070e672016-02-22 17:32:57 -08001463 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001464 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001465 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001466 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001467 extra_args = []
1468 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001469 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001470 if len(args) > 1:
1471 if args[1] == '--':
1472 if len(args) > 2:
1473 extra_args = args[2:]
1474 else:
1475 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001476
maruel380e3262016-08-31 16:10:06 -07001477 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001478 request = net.url_read_json(url)
1479 if not request:
1480 print >> sys.stderr, 'Failed to retrieve request data for the task'
1481 return 1
1482
maruel12e30012015-10-09 11:55:35 -07001483 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001484 if fs.isdir(workdir):
1485 parser.error('Please delete the directory \'work\' first')
1486 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001487 cachedir = unicode(os.path.abspath('cipd_cache'))
1488 if not fs.exists(cachedir):
1489 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001490
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001491 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001492 env = os.environ.copy()
1493 env['SWARMING_BOT_ID'] = 'reproduce'
1494 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001495 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001496 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001497 for i in properties['env']:
1498 key = i['key'].encode('utf-8')
1499 if not i['value']:
1500 env.pop(key, None)
1501 else:
1502 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001503
iannucci31ab9192017-05-02 19:11:56 -07001504 command = []
nodir152cba62016-05-12 16:08:56 -07001505 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001506 # Create the tree.
1507 with isolateserver.get_storage(
1508 properties['inputs_ref']['isolatedserver'],
1509 properties['inputs_ref']['namespace']) as storage:
1510 bundle = isolateserver.fetch_isolated(
1511 properties['inputs_ref']['isolated'],
1512 storage,
1513 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001514 workdir,
1515 False)
maruel29ab2fd2015-10-16 11:44:01 -07001516 command = bundle.command
1517 if bundle.relative_cwd:
1518 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001519 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001520
1521 if properties.get('command'):
1522 command.extend(properties['command'])
1523
1524 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1525 new_command = tools.fix_python_path(command)
1526 new_command = run_isolated.process_command(
1527 new_command, options.output_dir, None)
1528 if not options.output_dir and new_command != command:
1529 parser.error('The task has outputs, you must use --output-dir')
1530 command = new_command
1531 file_path.ensure_command_has_abs_path(command, workdir)
1532
1533 if properties.get('cipd_input'):
1534 ci = properties['cipd_input']
1535 cp = ci['client_package']
1536 client_manager = cipd.get_client(
1537 ci['server'], cp['package_name'], cp['version'], cachedir)
1538
1539 with client_manager as client:
1540 by_path = collections.defaultdict(list)
1541 for pkg in ci['packages']:
1542 path = pkg['path']
1543 # cipd deals with 'root' as ''
1544 if path == '.':
1545 path = ''
1546 by_path[path].append((pkg['package_name'], pkg['version']))
1547 client.ensure(workdir, by_path, cache_dir=cachedir)
1548
maruel77f720b2015-09-15 12:35:22 -07001549 try:
maruel18122c62015-10-23 06:31:23 -07001550 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001551 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001552 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001553 print >> sys.stderr, str(e)
1554 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001555
1556
maruel0eb1d1b2015-10-02 14:48:21 -07001557@subcommand.usage('bot_id')
1558def CMDterminate(parser, args):
1559 """Tells a bot to gracefully shut itself down as soon as it can.
1560
1561 This is done by completing whatever current task there is then exiting the bot
1562 process.
1563 """
1564 parser.add_option(
1565 '--wait', action='store_true', help='Wait for the bot to terminate')
1566 options, args = parser.parse_args(args)
1567 if len(args) != 1:
1568 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001569 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001570 request = net.url_read_json(url, data={})
1571 if not request:
1572 print >> sys.stderr, 'Failed to ask for termination'
1573 return 1
1574 if options.wait:
1575 return collect(
maruel9531ce02016-04-13 06:11:23 -07001576 options.swarming, [request['task_id']], 0., False, False, None, None,
1577 False)
maruelb7ded002017-06-10 16:43:17 -07001578 else:
1579 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001580 return 0
1581
1582
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001583@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001584def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001585 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001586
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001587 Passes all extra arguments provided after '--' as additional command line
1588 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001589 """
1590 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001591 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001592 parser.add_option(
1593 '--dump-json',
1594 metavar='FILE',
1595 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001596 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001597 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001598 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001599 tasks = trigger_task_shards(
1600 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001601 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001602 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001603 tasks_sorted = sorted(
1604 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001605 if options.dump_json:
1606 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001607 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001608 'tasks': tasks,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001609 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001610 }
maruel46b015f2015-10-13 18:40:35 -07001611 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001612 print('To collect results, use:')
1613 print(' swarming.py collect -S %s --json %s' %
1614 (options.swarming, options.dump_json))
1615 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001616 print('To collect results, use:')
1617 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001618 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1619 print('Or visit:')
1620 for t in tasks_sorted:
1621 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001622 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001623 except Failure:
1624 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001625 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001626
1627
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001628class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001629 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001630 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001631 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001632 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001633 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001634 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001635 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001636 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001637 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001638 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001639
1640 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001641 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001642 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001643 auth.process_auth_options(self, options)
1644 user = self._process_swarming(options)
1645 if hasattr(options, 'user') and not options.user:
1646 options.user = user
1647 return options, args
1648
1649 def _process_swarming(self, options):
1650 """Processes the --swarming option and aborts if not specified.
1651
1652 Returns the identity as determined by the server.
1653 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001654 if not options.swarming:
1655 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001656 try:
1657 options.swarming = net.fix_url(options.swarming)
1658 except ValueError as e:
1659 self.error('--swarming %s' % e)
1660 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001661 try:
1662 user = auth.ensure_logged_in(options.swarming)
1663 except ValueError as e:
1664 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001665 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001666
1667
1668def main(args):
1669 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001670 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001671
1672
1673if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001674 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001675 fix_encoding.fix_encoding()
1676 tools.disable_buffering()
1677 colorama.init()
1678 sys.exit(main(sys.argv[1:]))