blob: d67c2b358cf83e6dbfb5acfee76739a89317b241 [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,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050058 '_'.join(
59 '%s=%s' % (k, v)
maruela9fe2cb2017-05-10 10:43:23 -070060 for k, v in sorted(options.dimensions.iteritems())))
61 if options.isolated:
62 task_name += u'/' + options.isolated
63 return task_name
64 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050065
66
67### Triggering.
68
69
maruel77f720b2015-09-15 12:35:22 -070070# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070071CipdPackage = collections.namedtuple(
72 'CipdPackage',
73 [
74 'package_name',
75 'path',
76 'version',
77 ])
78
79
80# See ../appengine/swarming/swarming_rpcs.py.
81CipdInput = collections.namedtuple(
82 'CipdInput',
83 [
84 'client_package',
85 'packages',
86 'server',
87 ])
88
89
90# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070091FilesRef = collections.namedtuple(
92 'FilesRef',
93 [
94 'isolated',
95 'isolatedserver',
96 'namespace',
97 ])
98
99
100# See ../appengine/swarming/swarming_rpcs.py.
101TaskProperties = collections.namedtuple(
102 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500103 [
maruel681d6802017-01-17 16:56:03 -0800104 'caches',
borenet02f772b2016-06-22 12:42:19 -0700105 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500106 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500107 'dimensions',
108 'env',
maruel77f720b2015-09-15 12:35:22 -0700109 'execution_timeout_secs',
110 'extra_args',
111 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500112 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700113 'inputs_ref',
114 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700115 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700116 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700117 ])
118
119
120# See ../appengine/swarming/swarming_rpcs.py.
121NewTaskRequest = collections.namedtuple(
122 'NewTaskRequest',
123 [
124 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500125 'name',
maruel77f720b2015-09-15 12:35:22 -0700126 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500127 'priority',
maruel77f720b2015-09-15 12:35:22 -0700128 'properties',
vadimsh93d167c2016-09-13 11:31:51 -0700129 'service_account_token',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500130 'tags',
131 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500132 ])
133
134
maruel77f720b2015-09-15 12:35:22 -0700135def namedtuple_to_dict(value):
136 """Recursively converts a namedtuple to a dict."""
137 out = dict(value._asdict())
138 for k, v in out.iteritems():
139 if hasattr(v, '_asdict'):
140 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700141 elif isinstance(v, (list, tuple)):
142 l = []
143 for elem in v:
144 if hasattr(elem, '_asdict'):
145 l.append(namedtuple_to_dict(elem))
146 else:
147 l.append(elem)
148 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700149 return out
150
151
vadimsh93d167c2016-09-13 11:31:51 -0700152def task_request_to_raw_request(task_request, hide_token):
maruel71c61c82016-02-22 06:52:05 -0800153 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700154
155 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500156 """
maruel77f720b2015-09-15 12:35:22 -0700157 out = namedtuple_to_dict(task_request)
vadimsh93d167c2016-09-13 11:31:51 -0700158 if hide_token:
159 if out['service_account_token'] not in (None, 'bot', 'none'):
160 out['service_account_token'] = '<hidden>'
161 # Don't send 'service_account_token' if it is None to avoid confusing older
162 # version of the server that doesn't know about 'service_account_token'.
163 if out['service_account_token'] in (None, 'none'):
164 out.pop('service_account_token')
maruel77f720b2015-09-15 12:35:22 -0700165 # Maps are not supported until protobuf v3.
166 out['properties']['dimensions'] = [
167 {'key': k, 'value': v}
168 for k, v in out['properties']['dimensions'].iteritems()
169 ]
170 out['properties']['dimensions'].sort(key=lambda x: x['key'])
171 out['properties']['env'] = [
172 {'key': k, 'value': v}
173 for k, v in out['properties']['env'].iteritems()
174 ]
175 out['properties']['env'].sort(key=lambda x: x['key'])
176 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500177
178
maruel77f720b2015-09-15 12:35:22 -0700179def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500180 """Triggers a request on the Swarming server and returns the json data.
181
182 It's the low-level function.
183
184 Returns:
185 {
186 'request': {
187 'created_ts': u'2010-01-02 03:04:05',
188 'name': ..
189 },
190 'task_id': '12300',
191 }
192 """
193 logging.info('Triggering: %s', raw_request['name'])
194
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500195 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700196 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500197 if not result:
198 on_error.report('Failed to trigger task %s' % raw_request['name'])
199 return None
maruele557bce2015-11-17 09:01:27 -0800200 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800201 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800202 msg = 'Failed to trigger task %s' % raw_request['name']
203 if result['error'].get('errors'):
204 for err in result['error']['errors']:
205 if err.get('message'):
206 msg += '\nMessage: %s' % err['message']
207 if err.get('debugInfo'):
208 msg += '\nDebug info:\n%s' % err['debugInfo']
209 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800210 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800211
212 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800213 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500214 return result
215
216
217def setup_googletest(env, shards, index):
218 """Sets googletest specific environment variables."""
219 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700220 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
221 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
222 env = env[:]
223 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
224 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500225 return env
226
227
228def trigger_task_shards(swarming, task_request, shards):
229 """Triggers one or many subtasks of a sharded task.
230
231 Returns:
232 Dict with task details, returned to caller as part of --dump-json output.
233 None in case of failure.
234 """
235 def convert(index):
vadimsh93d167c2016-09-13 11:31:51 -0700236 req = task_request_to_raw_request(task_request, False)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500237 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700238 req['properties']['env'] = setup_googletest(
239 req['properties']['env'], shards, index)
240 req['name'] += ':%s:%s' % (index, shards)
241 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242
243 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500244 tasks = {}
245 priority_warning = False
246 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700247 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 if not task:
249 break
250 logging.info('Request result: %s', task)
251 if (not priority_warning and
252 task['request']['priority'] != task_request.priority):
253 priority_warning = True
254 print >> sys.stderr, (
255 'Priority was reset to %s' % task['request']['priority'])
256 tasks[request['name']] = {
257 'shard_index': index,
258 'task_id': task['task_id'],
259 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
260 }
261
262 # Some shards weren't triggered. Abort everything.
263 if len(tasks) != len(requests):
264 if tasks:
265 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
266 len(tasks), len(requests))
267 for task_dict in tasks.itervalues():
268 abort_task(swarming, task_dict['task_id'])
269 return None
270
271 return tasks
272
273
vadimsh93d167c2016-09-13 11:31:51 -0700274def mint_service_account_token(service_account):
275 """Given a service account name returns a delegation token for this account.
276
277 The token is generated based on triggering user's credentials. It is passed
278 to Swarming, that uses it when running tasks.
279 """
280 logging.info(
281 'Generating delegation token for service account "%s"', service_account)
282 raise NotImplementedError('Custom service accounts are not implemented yet')
283
284
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500285### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000286
287
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700288# How often to print status updates to stdout in 'collect'.
289STATUS_UPDATE_INTERVAL = 15 * 60.
290
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400291
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400292class State(object):
293 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000294
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400295 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
296 values are part of the API so if they change, the API changed.
297
298 It's in fact an enum. Values should be in decreasing order of importance.
299 """
300 RUNNING = 0x10
301 PENDING = 0x20
302 EXPIRED = 0x30
303 TIMED_OUT = 0x40
304 BOT_DIED = 0x50
305 CANCELED = 0x60
306 COMPLETED = 0x70
307
maruel77f720b2015-09-15 12:35:22 -0700308 STATES = (
309 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
310 'COMPLETED')
311 STATES_RUNNING = ('RUNNING', 'PENDING')
312 STATES_NOT_RUNNING = (
313 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
314 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
315 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400316
317 _NAMES = {
318 RUNNING: 'Running',
319 PENDING: 'Pending',
320 EXPIRED: 'Expired',
321 TIMED_OUT: 'Execution timed out',
322 BOT_DIED: 'Bot died',
323 CANCELED: 'User canceled',
324 COMPLETED: 'Completed',
325 }
326
maruel77f720b2015-09-15 12:35:22 -0700327 _ENUMS = {
328 'RUNNING': RUNNING,
329 'PENDING': PENDING,
330 'EXPIRED': EXPIRED,
331 'TIMED_OUT': TIMED_OUT,
332 'BOT_DIED': BOT_DIED,
333 'CANCELED': CANCELED,
334 'COMPLETED': COMPLETED,
335 }
336
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400337 @classmethod
338 def to_string(cls, state):
339 """Returns a user-readable string representing a State."""
340 if state not in cls._NAMES:
341 raise ValueError('Invalid state %s' % state)
342 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000343
maruel77f720b2015-09-15 12:35:22 -0700344 @classmethod
345 def from_enum(cls, state):
346 """Returns int value based on the string."""
347 if state not in cls._ENUMS:
348 raise ValueError('Invalid state %s' % state)
349 return cls._ENUMS[state]
350
maruel@chromium.org0437a732013-08-27 16:05:52 +0000351
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700352class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700353 """Assembles task execution summary (for --task-summary-json output).
354
355 Optionally fetches task outputs from isolate server to local disk (used when
356 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700357
358 This object is shared among multiple threads running 'retrieve_results'
359 function, in particular they call 'process_shard_result' method in parallel.
360 """
361
maruel0eb1d1b2015-10-02 14:48:21 -0700362 def __init__(self, task_output_dir, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
364
365 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700366 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700367 shard_count: expected number of task shards.
368 """
maruel12e30012015-10-09 11:55:35 -0700369 self.task_output_dir = (
370 unicode(os.path.abspath(task_output_dir))
371 if task_output_dir else task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 self.shard_count = shard_count
373
374 self._lock = threading.Lock()
375 self._per_shard_results = {}
376 self._storage = None
377
nodire5028a92016-04-29 14:38:21 -0700378 if self.task_output_dir:
379 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700380
Vadim Shtayurab450c602014-05-12 19:23:25 -0700381 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 """Stores results of a single task shard, fetches output files if necessary.
383
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400384 Modifies |result| in place.
385
maruel77f720b2015-09-15 12:35:22 -0700386 shard_index is 0-based.
387
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700388 Called concurrently from multiple threads.
389 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700390 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700391 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700392 if shard_index < 0 or shard_index >= self.shard_count:
393 logging.warning(
394 'Shard index %d is outside of expected range: [0; %d]',
395 shard_index, self.shard_count - 1)
396 return
397
maruel77f720b2015-09-15 12:35:22 -0700398 if result.get('outputs_ref'):
399 ref = result['outputs_ref']
400 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
401 ref['isolatedserver'],
402 urllib.urlencode(
403 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400404
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700405 # Store result dict of that shard, ignore results we've already seen.
406 with self._lock:
407 if shard_index in self._per_shard_results:
408 logging.warning('Ignoring duplicate shard index %d', shard_index)
409 return
410 self._per_shard_results[shard_index] = result
411
412 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700413 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400414 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700415 result['outputs_ref']['isolatedserver'],
416 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400417 if storage:
418 # Output files are supposed to be small and they are not reused across
419 # tasks. So use MemoryCache for them instead of on-disk cache. Make
420 # files writable, so that calling script can delete them.
421 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700422 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400423 storage,
424 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700425 os.path.join(self.task_output_dir, str(shard_index)),
426 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427
428 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700429 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430 with self._lock:
431 # Write an array of shard results with None for missing shards.
432 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433 'shards': [
434 self._per_shard_results.get(i) for i in xrange(self.shard_count)
435 ],
436 }
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700437 # Write summary.json to task_output_dir as well.
438 if self.task_output_dir:
439 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700440 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700441 summary,
442 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 if self._storage:
444 self._storage.close()
445 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700446 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700447
448 def _get_storage(self, isolate_server, namespace):
449 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700450 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700451 with self._lock:
452 if not self._storage:
453 self._storage = isolateserver.get_storage(isolate_server, namespace)
454 else:
455 # Shards must all use exact same isolate server and namespace.
456 if self._storage.location != isolate_server:
457 logging.error(
458 'Task shards are using multiple isolate servers: %s and %s',
459 self._storage.location, isolate_server)
460 return None
461 if self._storage.namespace != namespace:
462 logging.error(
463 'Task shards are using multiple namespaces: %s and %s',
464 self._storage.namespace, namespace)
465 return None
466 return self._storage
467
468
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500469def now():
470 """Exists so it can be mocked easily."""
471 return time.time()
472
473
maruel77f720b2015-09-15 12:35:22 -0700474def parse_time(value):
475 """Converts serialized time from the API to datetime.datetime."""
476 # When microseconds are 0, the '.123456' suffix is elided. This means the
477 # serialized format is not consistent, which confuses the hell out of python.
478 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
479 try:
480 return datetime.datetime.strptime(value, fmt)
481 except ValueError:
482 pass
483 raise ValueError('Failed to parse %s' % value)
484
485
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700486def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700487 base_url, shard_index, task_id, timeout, should_stop, output_collector,
488 include_perf):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400489 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700490
Vadim Shtayurab450c602014-05-12 19:23:25 -0700491 Returns:
492 <result dict> on success.
493 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494 """
maruel71c61c82016-02-22 06:52:05 -0800495 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700496 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700497 if include_perf:
498 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700499 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700500 started = now()
501 deadline = started + timeout if timeout else None
502 attempt = 0
503
504 while not should_stop.is_set():
505 attempt += 1
506
507 # Waiting for too long -> give up.
508 current_time = now()
509 if deadline and current_time >= deadline:
510 logging.error('retrieve_results(%s) timed out on attempt %d',
511 base_url, attempt)
512 return None
513
514 # Do not spin too fast. Spin faster at the beginning though.
515 # Start with 1 sec delay and for each 30 sec of waiting add another second
516 # of delay, until hitting 15 sec ceiling.
517 if attempt > 1:
518 max_delay = min(15, 1 + (current_time - started) / 30.0)
519 delay = min(max_delay, deadline - current_time) if deadline else max_delay
520 if delay > 0:
521 logging.debug('Waiting %.1f sec before retrying', delay)
522 should_stop.wait(delay)
523 if should_stop.is_set():
524 return None
525
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400526 # Disable internal retries in net.url_read_json, since we are doing retries
527 # ourselves.
528 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700529 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
530 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 result = net.url_read_json(result_url, retry_50x=False)
532 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400533 continue
maruel77f720b2015-09-15 12:35:22 -0700534
maruelbf53e042015-12-01 15:00:51 -0800535 if result.get('error'):
536 # An error occurred.
537 if result['error'].get('errors'):
538 for err in result['error']['errors']:
539 logging.warning(
540 'Error while reading task: %s; %s',
541 err.get('message'), err.get('debugInfo'))
542 elif result['error'].get('message'):
543 logging.warning(
544 'Error while reading task: %s', result['error']['message'])
545 continue
546
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400547 if result['state'] in State.STATES_NOT_RUNNING:
maruel77f720b2015-09-15 12:35:22 -0700548 # TODO(maruel): Not always fetch stdout?
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400549 out = net.url_read_json(output_url)
maruel77f720b2015-09-15 12:35:22 -0700550 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700551 # Record the result, try to fetch attached output files (if any).
552 if output_collector:
553 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700554 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700555 if result.get('internal_failure'):
556 logging.error('Internal error!')
557 elif result['state'] == 'BOT_DIED':
558 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700559 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000560
561
maruel77f720b2015-09-15 12:35:22 -0700562def convert_to_old_format(result):
563 """Converts the task result data from Endpoints API format to old API format
564 for compatibility.
565
566 This goes into the file generated as --task-summary-json.
567 """
568 # Sets default.
569 result.setdefault('abandoned_ts', None)
570 result.setdefault('bot_id', None)
571 result.setdefault('bot_version', None)
572 result.setdefault('children_task_ids', [])
573 result.setdefault('completed_ts', None)
574 result.setdefault('cost_saved_usd', None)
575 result.setdefault('costs_usd', None)
576 result.setdefault('deduped_from', None)
577 result.setdefault('name', None)
578 result.setdefault('outputs_ref', None)
579 result.setdefault('properties_hash', None)
580 result.setdefault('server_versions', None)
581 result.setdefault('started_ts', None)
582 result.setdefault('tags', None)
583 result.setdefault('user', None)
584
585 # Convertion back to old API.
586 duration = result.pop('duration', None)
587 result['durations'] = [duration] if duration else []
588 exit_code = result.pop('exit_code', None)
589 result['exit_codes'] = [int(exit_code)] if exit_code else []
590 result['id'] = result.pop('task_id')
591 result['isolated_out'] = result.get('outputs_ref', None)
592 output = result.pop('output', None)
593 result['outputs'] = [output] if output else []
594 # properties_hash
595 # server_version
596 # Endpoints result 'state' as string. For compatibility with old code, convert
597 # to int.
598 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700599 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700600 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700601 if 'bot_dimensions' in result:
602 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700603 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700604 }
605 else:
606 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700607
608
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700609def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400610 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700611 output_collector, include_perf):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500612 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000613
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700614 Duplicate shards are ignored. Shards are yielded in order of completion.
615 Timed out shards are NOT yielded at all. Caller can compare number of yielded
616 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000617
618 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500619 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 +0000620 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500621
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700622 output_collector is an optional instance of TaskOutputCollector that will be
623 used to fetch files produced by a task from isolate server to the local disk.
624
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500625 Yields:
626 (index, result). In particular, 'result' is defined as the
627 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000629 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400630 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700631 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700632 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700633
maruel@chromium.org0437a732013-08-27 16:05:52 +0000634 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
635 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700636 # Adds a task to the thread pool to call 'retrieve_results' and return
637 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400638 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
maruel9531ce02016-04-13 06:11:23 -0700642 task_id, timeout, should_stop, output_collector, include_perf)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700643
644 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400645 for shard_index, task_id in enumerate(task_ids):
646 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700647
648 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400649 shards_remaining = range(len(task_ids))
650 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700651 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700653 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654 shard_index, result = results_channel.pull(
655 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700656 except threading_utils.TaskChannel.Timeout:
657 if print_status_updates:
658 print(
659 'Waiting for results from the following shards: %s' %
660 ', '.join(map(str, shards_remaining)))
661 sys.stdout.flush()
662 continue
663 except Exception:
664 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700665
666 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700667 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000668 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500669 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000670 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700671
Vadim Shtayurab450c602014-05-12 19:23:25 -0700672 # Yield back results to the caller.
673 assert shard_index in shards_remaining
674 shards_remaining.remove(shard_index)
675 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700676
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 should_stop.set()
680
681
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400682def decorate_shard_output(swarming, shard_index, metadata):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000683 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700684 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400685 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700686 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
687 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400688 else:
689 pending = 'N/A'
690
maruel77f720b2015-09-15 12:35:22 -0700691 if metadata.get('duration') is not None:
692 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400693 else:
694 duration = 'N/A'
695
maruel77f720b2015-09-15 12:35:22 -0700696 if metadata.get('exit_code') is not None:
697 # Integers are encoded as string to not loose precision.
698 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400699 else:
700 exit_code = 'N/A'
701
702 bot_id = metadata.get('bot_id') or 'N/A'
703
maruel77f720b2015-09-15 12:35:22 -0700704 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400705 tag_header = 'Shard %d %s' % (shard_index, url)
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400706 tag_footer = (
707 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % (
708 shard_index, pending, duration, bot_id, exit_code))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400709
710 tag_len = max(len(tag_header), len(tag_footer))
711 dash_pad = '+-%s-+\n' % ('-' * tag_len)
712 tag_header = '| %s |\n' % tag_header.ljust(tag_len)
713 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len)
714
715 header = dash_pad + tag_header + dash_pad
716 footer = dash_pad + tag_footer + dash_pad[:-1]
maruel1d87b2f2015-09-16 06:51:07 -0700717 output = (metadata.get('output') or '').rstrip() + '\n'
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400718 return header + output + footer
maruel@chromium.org0437a732013-08-27 16:05:52 +0000719
720
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700721def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700722 swarming, task_ids, timeout, decorate, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700723 task_summary_json, task_output_dir, include_perf):
maruela5490782015-09-30 10:56:59 -0700724 """Retrieves results of a Swarming task.
725
726 Returns:
727 process exit code that should be returned to the user.
728 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700729 # Collect summary JSON and output files (if task_output_dir is not None).
maruel0eb1d1b2015-10-02 14:48:21 -0700730 output_collector = TaskOutputCollector(task_output_dir, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700732 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700733 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400734 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700735 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400736 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400737 swarming, task_ids, timeout, None, print_status_updates,
maruel9531ce02016-04-13 06:11:23 -0700738 output_collector, include_perf):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700739 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700740
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400741 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700742 shard_exit_code = metadata.get('exit_code')
743 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700744 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700745 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700746 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400747 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700748 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700749
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700750 if decorate:
leileied181762016-10-13 14:24:59 -0700751 s = decorate_shard_output(swarming, index, metadata).encode(
752 'utf-8', 'replace')
753 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400754 if len(seen_shards) < len(task_ids):
755 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700756 else:
maruel77f720b2015-09-15 12:35:22 -0700757 print('%s: %s %s' % (
758 metadata.get('bot_id', 'N/A'),
759 metadata['task_id'],
760 shard_exit_code))
761 if metadata['output']:
762 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400763 if output:
764 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700765 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700766 summary = output_collector.finalize()
767 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700768 # TODO(maruel): Make this optional.
769 for i in summary['shards']:
770 if i:
771 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700772 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400774 if decorate and total_duration:
775 print('Total duration: %.1fs' % total_duration)
776
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400777 if len(seen_shards) != len(task_ids):
778 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700779 print >> sys.stderr, ('Results from some shards are missing: %s' %
780 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700781 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782
maruela5490782015-09-30 10:56:59 -0700783 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000784
785
maruel77f720b2015-09-15 12:35:22 -0700786### API management.
787
788
789class APIError(Exception):
790 pass
791
792
793def endpoints_api_discovery_apis(host):
794 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
795 the APIs exposed by a host.
796
797 https://developers.google.com/discovery/v1/reference/apis/list
798 """
maruel380e3262016-08-31 16:10:06 -0700799 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
800 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700801 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
802 if data is None:
803 raise APIError('Failed to discover APIs on %s' % host)
804 out = {}
805 for api in data['items']:
806 if api['id'] == 'discovery:v1':
807 continue
808 # URL is of the following form:
809 # url = host + (
810 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
811 api_data = net.url_read_json(api['discoveryRestUrl'])
812 if api_data is None:
813 raise APIError('Failed to discover %s on %s' % (api['id'], host))
814 out[api['id']] = api_data
815 return out
816
817
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500818### Commands.
819
820
821def abort_task(_swarming, _manifest):
822 """Given a task manifest that was triggered, aborts its execution."""
823 # TODO(vadimsh): No supported by the server yet.
824
825
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400826def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800827 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500828 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500829 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500830 dest='dimensions', metavar='FOO bar',
831 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500832 parser.add_option_group(parser.filter_group)
833
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400834
Vadim Shtayurab450c602014-05-12 19:23:25 -0700835def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400836 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700837 parser.sharding_group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700838 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700839 help='Number of shards to trigger and collect.')
840 parser.add_option_group(parser.sharding_group)
841
842
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400843def add_trigger_options(parser):
844 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500845 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400846 add_filter_options(parser)
847
maruel681d6802017-01-17 16:56:03 -0800848 group = optparse.OptionGroup(parser, 'Task properties')
849 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700850 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500851 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800852 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500853 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700854 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800855 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400856 '--idempotent', action='store_true', default=False,
857 help='When set, the server will actively try to find a previous task '
858 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800859 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700860 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700861 help='The optional path to a file containing the secret_bytes to use with'
862 'this task.')
maruel681d6802017-01-17 16:56:03 -0800863 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700864 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400865 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800866 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700867 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400868 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800869 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500870 '--raw-cmd', action='store_true', default=False,
871 help='When set, the command after -- is used as-is without run_isolated. '
maruela9fe2cb2017-05-10 10:43:23 -0700872 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800873 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700874 '--cipd-package', action='append', default=[], metavar='PKG',
875 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700876 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800877 group.add_option(
878 '--named-cache', action='append', nargs=2, default=[],
maruel3773d8c2017-05-31 15:35:47 -0700879 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800880 help='"<name> <relpath>" items to keep a persistent bot managed cache')
881 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700882 '--service-account',
883 help='Name of a service account to run the task as. Only literal "bot" '
884 'string can be specified currently (to run the task under bot\'s '
885 'account). Don\'t use task service accounts if not given '
886 '(default).')
maruel681d6802017-01-17 16:56:03 -0800887 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700888 '-o', '--output', action='append', default=[], metavar='PATH',
889 help='A list of files to return in addition to those written to '
890 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
891 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -0800892 parser.add_option_group(group)
893
894 group = optparse.OptionGroup(parser, 'Task request')
895 group.add_option(
896 '--priority', type='int', default=100,
897 help='The lower value, the more important the task is')
898 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700899 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -0800900 help='Display name of the task. Defaults to '
901 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
902 'isolated file is provided, if a hash is provided, it defaults to '
903 '<user>/<dimensions>/<isolated hash>/<timestamp>')
904 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700905 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -0800906 help='Tags to assign to the task.')
907 group.add_option(
908 '--user', default='',
909 help='User associated with the task. Defaults to authenticated user on '
910 'the server.')
911 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700912 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -0800913 help='Seconds to allow the task to be pending for a bot to run before '
914 'this task request expires.')
915 group.add_option(
916 '--deadline', type='int', dest='expiration',
917 help=optparse.SUPPRESS_HELP)
918 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000919
920
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500921def process_trigger_options(parser, options, args):
vadimsh93d167c2016-09-13 11:31:51 -0700922 """Processes trigger options and does preparatory steps.
923
maruel4e901792017-05-09 12:07:02 -0700924 Generates service account tokens if necessary.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500925 """
926 options.dimensions = dict(options.dimensions)
927 options.env = dict(options.env)
maruela9fe2cb2017-05-10 10:43:23 -0700928 if args and args[0] == '--':
929 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500930
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500931 if not options.dimensions:
932 parser.error('Please at least specify one --dimension')
maruela9fe2cb2017-05-10 10:43:23 -0700933 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
934 parser.error('--tags must be in the format key:value')
935 if options.raw_cmd and not args:
936 parser.error(
937 'Arguments with --raw-cmd should be passed after -- as command '
938 'delimiter.')
939 if options.isolate_server and not options.namespace:
940 parser.error(
941 '--namespace must be a valid value when --isolate-server is used')
942 if not options.isolated and not options.raw_cmd:
943 parser.error('Specify at least one of --raw-cmd or --isolated or both')
944
945 # Isolated
946 # --isolated is required only if --raw-cmd wasn't provided.
947 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
948 # preferred server.
949 isolateserver.process_isolate_server_options(
950 parser, options, False, not options.raw_cmd)
951 inputs_ref = None
952 if options.isolate_server:
953 inputs_ref = FilesRef(
954 isolated=options.isolated,
955 isolatedserver=options.isolate_server,
956 namespace=options.namespace)
957
958 # Command
959 command = None
960 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500961 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500962 command = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500963 else:
maruela9fe2cb2017-05-10 10:43:23 -0700964 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500965
maruela9fe2cb2017-05-10 10:43:23 -0700966 # CIPD
borenet02f772b2016-06-22 12:42:19 -0700967 cipd_packages = []
968 for p in options.cipd_package:
969 split = p.split(':', 2)
970 if len(split) != 3:
971 parser.error('CIPD packages must take the form: path:package:version')
972 cipd_packages.append(CipdPackage(
973 package_name=split[1],
974 path=split[0],
975 version=split[2]))
976 cipd_input = None
977 if cipd_packages:
978 cipd_input = CipdInput(
979 client_package=None,
980 packages=cipd_packages,
981 server=None)
982
maruela9fe2cb2017-05-10 10:43:23 -0700983 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -0700984 secret_bytes = None
985 if options.secret_bytes_path:
986 with open(options.secret_bytes_path, 'r') as f:
987 secret_bytes = f.read().encode('base64')
988
maruela9fe2cb2017-05-10 10:43:23 -0700989 # Named caches
maruel681d6802017-01-17 16:56:03 -0800990 caches = [
991 {u'name': unicode(i[0]), u'path': unicode(i[1])}
992 for i in options.named_cache
993 ]
maruela9fe2cb2017-05-10 10:43:23 -0700994
maruel77f720b2015-09-15 12:35:22 -0700995 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -0800996 caches=caches,
borenet02f772b2016-06-22 12:42:19 -0700997 cipd_input=cipd_input,
maruela9fe2cb2017-05-10 10:43:23 -0700998 command=command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500999 dimensions=options.dimensions,
1000 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001001 execution_timeout_secs=options.hard_timeout,
maruela9fe2cb2017-05-10 10:43:23 -07001002 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001003 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001004 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001005 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001006 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001007 outputs=options.output,
1008 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001009
1010 # Convert a service account email to a signed service account token to pass
1011 # to Swarming.
1012 service_account_token = None
1013 if options.service_account in ('bot', 'none'):
1014 service_account_token = options.service_account
1015 elif options.service_account:
1016 # pylint: disable=assignment-from-no-return
1017 service_account_token = mint_service_account_token(options.service_account)
1018
maruel77f720b2015-09-15 12:35:22 -07001019 return NewTaskRequest(
1020 expiration_secs=options.expiration,
maruela9fe2cb2017-05-10 10:43:23 -07001021 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001022 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001023 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001024 properties=properties,
vadimsh93d167c2016-09-13 11:31:51 -07001025 service_account_token=service_account_token,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001026 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001027 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001028
1029
1030def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001031 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001032 '-t', '--timeout', type='float',
1033 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1034 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001035 parser.group_logging.add_option(
1036 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001037 parser.group_logging.add_option(
1038 '--print-status-updates', action='store_true',
1039 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001040 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001041 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001042 '--task-summary-json',
1043 metavar='FILE',
1044 help='Dump a summary of task results to this file as json. It contains '
1045 'only shards statuses as know to server directly. Any output files '
1046 'emitted by the task can be collected by using --task-output-dir')
1047 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001048 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001049 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001050 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001051 'directory contains per-shard directory with output files produced '
1052 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
maruel9531ce02016-04-13 06:11:23 -07001053 parser.task_output_group.add_option(
1054 '--perf', action='store_true', default=False,
1055 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001056 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001057
1058
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001059@subcommand.usage('bots...')
1060def CMDbot_delete(parser, args):
1061 """Forcibly deletes bots from the Swarming server."""
1062 parser.add_option(
1063 '-f', '--force', action='store_true',
1064 help='Do not prompt for confirmation')
1065 options, args = parser.parse_args(args)
1066 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001067 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001068
1069 bots = sorted(args)
1070 if not options.force:
1071 print('Delete the following bots?')
1072 for bot in bots:
1073 print(' %s' % bot)
1074 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1075 print('Goodbye.')
1076 return 1
1077
1078 result = 0
1079 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001080 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001081 if net.url_read_json(url, data={}, method='POST') is None:
1082 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001083 result = 1
1084 return result
1085
1086
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001087def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001088 """Returns information about the bots connected to the Swarming server."""
1089 add_filter_options(parser)
1090 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001091 '--dead-only', action='store_true',
1092 help='Only print dead bots, useful to reap them and reimage broken bots')
1093 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001094 '-k', '--keep-dead', action='store_true',
1095 help='Do not filter out dead bots')
1096 parser.filter_group.add_option(
1097 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001098 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001099 options, args = parser.parse_args(args)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001100
1101 if options.keep_dead and options.dead_only:
1102 parser.error('Use only one of --keep-dead and --dead-only')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001103
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001104 bots = []
1105 cursor = None
1106 limit = 250
1107 # Iterate via cursors.
maruel77f720b2015-09-15 12:35:22 -07001108 base_url = (
maruel380e3262016-08-31 16:10:06 -07001109 options.swarming + '/api/swarming/v1/bots/list?limit=%d' % limit)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001110 while True:
1111 url = base_url
1112 if cursor:
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001113 url += '&cursor=%s' % urllib.quote(cursor)
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001114 data = net.url_read_json(url)
1115 if data is None:
1116 print >> sys.stderr, 'Failed to access %s' % options.swarming
1117 return 1
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001118 bots.extend(data['items'])
maruel77f720b2015-09-15 12:35:22 -07001119 cursor = data.get('cursor')
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001120 if not cursor:
1121 break
1122
maruel77f720b2015-09-15 12:35:22 -07001123 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001124 if options.dead_only:
maruel77f720b2015-09-15 12:35:22 -07001125 if not bot.get('is_dead'):
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001126 continue
maruel77f720b2015-09-15 12:35:22 -07001127 elif not options.keep_dead and bot.get('is_dead'):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001128 continue
1129
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001130 # If the user requested to filter on dimensions, ensure the bot has all the
1131 # dimensions requested.
smut6aab7cd2016-10-12 10:38:11 -07001132 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001133 for key, value in options.dimensions:
1134 if key not in dimensions:
1135 break
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001136 # A bot can have multiple value for a key, for example,
1137 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will
1138 # be accepted.
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001139 if isinstance(dimensions[key], list):
1140 if value not in dimensions[key]:
1141 break
1142 else:
1143 if value != dimensions[key]:
1144 break
1145 else:
maruel77f720b2015-09-15 12:35:22 -07001146 print bot['bot_id']
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001147 if not options.bare:
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -04001148 print ' %s' % json.dumps(dimensions, sort_keys=True)
Marc-Antoine Ruelfd491172014-11-19 19:26:13 -05001149 if bot.get('task_id'):
1150 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001151 return 0
1152
1153
maruelfd0a90c2016-06-10 11:51:10 -07001154@subcommand.usage('task_id')
1155def CMDcancel(parser, args):
1156 """Cancels a task."""
1157 options, args = parser.parse_args(args)
1158 if not args:
1159 parser.error('Please specify the task to cancel')
1160 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001161 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001162 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1163 print('Deleting %s failed. Probably already gone' % task_id)
1164 return 1
1165 return 0
1166
1167
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001168@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001169def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001170 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001171
1172 The result can be in multiple part if the execution was sharded. It can
1173 potentially have retries.
1174 """
1175 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001176 parser.add_option(
1177 '-j', '--json',
1178 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001179 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001180 if not args and not options.json:
1181 parser.error('Must specify at least one task id or --json.')
1182 if args and options.json:
1183 parser.error('Only use one of task id or --json.')
1184
1185 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001186 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001187 try:
maruel1ceb3872015-10-14 06:10:44 -07001188 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001189 data = json.load(f)
1190 except (IOError, ValueError):
1191 parser.error('Failed to open %s' % options.json)
1192 try:
1193 tasks = sorted(
1194 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1195 args = [t['task_id'] for t in tasks]
1196 except (KeyError, TypeError):
1197 parser.error('Failed to process %s' % options.json)
1198 if options.timeout is None:
1199 options.timeout = (
1200 data['request']['properties']['execution_timeout_secs'] +
1201 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001202 else:
1203 valid = frozenset('0123456789abcdef')
1204 if any(not valid.issuperset(task_id) for task_id in args):
1205 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001206
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001207 try:
1208 return collect(
1209 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001210 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001211 options.timeout,
1212 options.decorate,
1213 options.print_status_updates,
1214 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001215 options.task_output_dir,
1216 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001217 except Failure:
1218 on_error.report(None)
1219 return 1
1220
1221
maruelbea00862015-09-18 09:55:36 -07001222@subcommand.usage('[filename]')
1223def CMDput_bootstrap(parser, args):
1224 """Uploads a new version of bootstrap.py."""
1225 options, args = parser.parse_args(args)
1226 if len(args) != 1:
1227 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001228 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001229 path = unicode(os.path.abspath(args[0]))
1230 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001231 content = f.read().decode('utf-8')
1232 data = net.url_read_json(url, data={'content': content})
1233 print data
1234 return 0
1235
1236
1237@subcommand.usage('[filename]')
1238def CMDput_bot_config(parser, args):
1239 """Uploads a new version of bot_config.py."""
1240 options, args = parser.parse_args(args)
1241 if len(args) != 1:
1242 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001243 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001244 path = unicode(os.path.abspath(args[0]))
1245 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001246 content = f.read().decode('utf-8')
1247 data = net.url_read_json(url, data={'content': content})
1248 print data
1249 return 0
1250
1251
maruel77f720b2015-09-15 12:35:22 -07001252@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001253def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001254 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1255 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001256
1257 Examples:
maruel77f720b2015-09-15 12:35:22 -07001258 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001259 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001260
maruel77f720b2015-09-15 12:35:22 -07001261 Listing last 10 tasks on a specific bot named 'swarm1':
maruel84e77aa2015-10-21 06:37:24 -07001262 swarming.py query -S server-url.com --limit 10 bot/swarm1/tasks
1263
1264 Listing last 10 tasks with tags os:Ubuntu-12.04 and pool:Chrome. Note that
1265 quoting is important!:
1266 swarming.py query -S server-url.com --limit 10 \\
1267 'tasks/list?tags=os:Ubuntu-12.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001268 """
1269 CHUNK_SIZE = 250
1270
1271 parser.add_option(
1272 '-L', '--limit', type='int', default=200,
1273 help='Limit to enforce on limitless items (like number of tasks); '
1274 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001275 parser.add_option(
1276 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001277 parser.add_option(
1278 '--progress', action='store_true',
1279 help='Prints a dot at each request to show progress')
1280 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001281 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001282 parser.error(
1283 'Must specify only method name and optionally query args properly '
1284 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001285 base_url = options.swarming + '/api/swarming/v1/' + args[0]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001286 url = base_url
1287 if options.limit:
Marc-Antoine Ruelea74f292014-10-24 20:55:39 -04001288 # Check check, change if not working out.
1289 merge_char = '&' if '?' in url else '?'
1290 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit))
marueld8aba222015-09-03 12:21:19 -07001291 data = net.url_read_json(url)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001292 if data is None:
maruel77f720b2015-09-15 12:35:22 -07001293 # TODO(maruel): Do basic diagnostic.
1294 print >> sys.stderr, 'Failed to access %s' % url
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001295 return 1
1296
1297 # Some items support cursors. Try to get automatically if cursors are needed
1298 # by looking at the 'cursor' items.
1299 while (
1300 data.get('cursor') and
1301 (not options.limit or len(data['items']) < options.limit)):
Marc-Antoine Ruel0696e402015-03-23 15:28:44 -04001302 merge_char = '&' if '?' in base_url else '?'
1303 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor']))
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001304 if options.limit:
1305 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items']))
maruel77f720b2015-09-15 12:35:22 -07001306 if options.progress:
1307 sys.stdout.write('.')
1308 sys.stdout.flush()
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001309 new = net.url_read_json(url)
1310 if new is None:
maruel77f720b2015-09-15 12:35:22 -07001311 if options.progress:
1312 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001313 print >> sys.stderr, 'Failed to access %s' % options.swarming
1314 return 1
maruel81b37132015-10-21 06:42:13 -07001315 data['items'].extend(new.get('items', []))
maruel77f720b2015-09-15 12:35:22 -07001316 data['cursor'] = new.get('cursor')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001317
maruel77f720b2015-09-15 12:35:22 -07001318 if options.progress:
1319 print('')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001320 if options.limit and len(data.get('items', [])) > options.limit:
1321 data['items'] = data['items'][:options.limit]
1322 data.pop('cursor', None)
1323
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001324 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001325 options.json = unicode(os.path.abspath(options.json))
1326 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001327 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001328 try:
maruel77f720b2015-09-15 12:35:22 -07001329 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001330 sys.stdout.write('\n')
1331 except IOError:
1332 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001333 return 0
1334
1335
maruel77f720b2015-09-15 12:35:22 -07001336def CMDquery_list(parser, args):
1337 """Returns list of all the Swarming APIs that can be used with command
1338 'query'.
1339 """
1340 parser.add_option(
1341 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1342 options, args = parser.parse_args(args)
1343 if args:
1344 parser.error('No argument allowed.')
1345
1346 try:
1347 apis = endpoints_api_discovery_apis(options.swarming)
1348 except APIError as e:
1349 parser.error(str(e))
1350 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001351 options.json = unicode(os.path.abspath(options.json))
1352 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001353 json.dump(apis, f)
1354 else:
1355 help_url = (
1356 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1357 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001358 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1359 if i:
1360 print('')
maruel77f720b2015-09-15 12:35:22 -07001361 print api_id
maruel11e31af2017-02-15 07:30:50 -08001362 print ' ' + api['description'].strip()
1363 if 'resources' in api:
1364 # Old.
1365 for j, (resource_name, resource) in enumerate(
1366 sorted(api['resources'].iteritems())):
1367 if j:
1368 print('')
1369 for method_name, method in sorted(resource['methods'].iteritems()):
1370 # Only list the GET ones.
1371 if method['httpMethod'] != 'GET':
1372 continue
1373 print '- %s.%s: %s' % (
1374 resource_name, method_name, method['path'])
1375 print('\n'.join(
1376 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1377 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1378 else:
1379 # New.
1380 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001381 # Only list the GET ones.
1382 if method['httpMethod'] != 'GET':
1383 continue
maruel11e31af2017-02-15 07:30:50 -08001384 print '- %s: %s' % (method['id'], method['path'])
1385 print('\n'.join(
1386 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001387 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1388 return 0
1389
1390
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001391@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001392def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001393 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001394
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001395 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001396 """
1397 add_trigger_options(parser)
1398 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001399 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001400 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001401 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001402 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001403 tasks = trigger_task_shards(
1404 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001405 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001406 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001407 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001408 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001409 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001410 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001411 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001412 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001413 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001414 task_ids = [
1415 t['task_id']
1416 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1417 ]
maruel71c61c82016-02-22 06:52:05 -08001418 if options.timeout is None:
1419 options.timeout = (
1420 task_request.properties.execution_timeout_secs +
1421 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001422 try:
1423 return collect(
1424 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001425 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001426 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001427 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001428 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001429 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001430 options.task_output_dir,
1431 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001432 except Failure:
1433 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001434 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001435
1436
maruel18122c62015-10-23 06:31:23 -07001437@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001438def CMDreproduce(parser, args):
1439 """Runs a task locally that was triggered on the server.
1440
1441 This running locally the same commands that have been run on the bot. The data
1442 downloaded will be in a subdirectory named 'work' of the current working
1443 directory.
maruel18122c62015-10-23 06:31:23 -07001444
1445 You can pass further additional arguments to the target command by passing
1446 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001447 """
maruelc070e672016-02-22 17:32:57 -08001448 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001449 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001450 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001451 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001452 extra_args = []
1453 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001454 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001455 if len(args) > 1:
1456 if args[1] == '--':
1457 if len(args) > 2:
1458 extra_args = args[2:]
1459 else:
1460 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001461
maruel380e3262016-08-31 16:10:06 -07001462 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001463 request = net.url_read_json(url)
1464 if not request:
1465 print >> sys.stderr, 'Failed to retrieve request data for the task'
1466 return 1
1467
maruel12e30012015-10-09 11:55:35 -07001468 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001469 if fs.isdir(workdir):
1470 parser.error('Please delete the directory \'work\' first')
1471 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001472 cachedir = unicode(os.path.abspath('cipd_cache'))
1473 if not fs.exists(cachedir):
1474 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001475
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001476 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001477 env = os.environ.copy()
1478 env['SWARMING_BOT_ID'] = 'reproduce'
1479 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001480 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001481 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001482 for i in properties['env']:
1483 key = i['key'].encode('utf-8')
1484 if not i['value']:
1485 env.pop(key, None)
1486 else:
1487 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001488
iannucci31ab9192017-05-02 19:11:56 -07001489 command = []
nodir152cba62016-05-12 16:08:56 -07001490 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001491 # Create the tree.
1492 with isolateserver.get_storage(
1493 properties['inputs_ref']['isolatedserver'],
1494 properties['inputs_ref']['namespace']) as storage:
1495 bundle = isolateserver.fetch_isolated(
1496 properties['inputs_ref']['isolated'],
1497 storage,
1498 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001499 workdir,
1500 False)
maruel29ab2fd2015-10-16 11:44:01 -07001501 command = bundle.command
1502 if bundle.relative_cwd:
1503 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001504 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001505
1506 if properties.get('command'):
1507 command.extend(properties['command'])
1508
1509 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1510 new_command = tools.fix_python_path(command)
1511 new_command = run_isolated.process_command(
1512 new_command, options.output_dir, None)
1513 if not options.output_dir and new_command != command:
1514 parser.error('The task has outputs, you must use --output-dir')
1515 command = new_command
1516 file_path.ensure_command_has_abs_path(command, workdir)
1517
1518 if properties.get('cipd_input'):
1519 ci = properties['cipd_input']
1520 cp = ci['client_package']
1521 client_manager = cipd.get_client(
1522 ci['server'], cp['package_name'], cp['version'], cachedir)
1523
1524 with client_manager as client:
1525 by_path = collections.defaultdict(list)
1526 for pkg in ci['packages']:
1527 path = pkg['path']
1528 # cipd deals with 'root' as ''
1529 if path == '.':
1530 path = ''
1531 by_path[path].append((pkg['package_name'], pkg['version']))
1532 client.ensure(workdir, by_path, cache_dir=cachedir)
1533
maruel77f720b2015-09-15 12:35:22 -07001534 try:
maruel18122c62015-10-23 06:31:23 -07001535 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001536 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001537 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001538 print >> sys.stderr, str(e)
1539 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001540
1541
maruel0eb1d1b2015-10-02 14:48:21 -07001542@subcommand.usage('bot_id')
1543def CMDterminate(parser, args):
1544 """Tells a bot to gracefully shut itself down as soon as it can.
1545
1546 This is done by completing whatever current task there is then exiting the bot
1547 process.
1548 """
1549 parser.add_option(
1550 '--wait', action='store_true', help='Wait for the bot to terminate')
1551 options, args = parser.parse_args(args)
1552 if len(args) != 1:
1553 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001554 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001555 request = net.url_read_json(url, data={})
1556 if not request:
1557 print >> sys.stderr, 'Failed to ask for termination'
1558 return 1
1559 if options.wait:
1560 return collect(
maruel9531ce02016-04-13 06:11:23 -07001561 options.swarming, [request['task_id']], 0., False, False, None, None,
1562 False)
maruel0eb1d1b2015-10-02 14:48:21 -07001563 return 0
1564
1565
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001566@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001567def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001568 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001569
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001570 Passes all extra arguments provided after '--' as additional command line
1571 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001572 """
1573 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001574 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001575 parser.add_option(
1576 '--dump-json',
1577 metavar='FILE',
1578 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001579 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001580 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001581 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001582 tasks = trigger_task_shards(
1583 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001584 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001585 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001586 tasks_sorted = sorted(
1587 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001588 if options.dump_json:
1589 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001590 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001591 'tasks': tasks,
vadimsh93d167c2016-09-13 11:31:51 -07001592 'request': task_request_to_raw_request(task_request, True),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001593 }
maruel46b015f2015-10-13 18:40:35 -07001594 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001595 print('To collect results, use:')
1596 print(' swarming.py collect -S %s --json %s' %
1597 (options.swarming, options.dump_json))
1598 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001599 print('To collect results, use:')
1600 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001601 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1602 print('Or visit:')
1603 for t in tasks_sorted:
1604 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001605 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001606 except Failure:
1607 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001608 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001609
1610
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001611class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001612 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001613 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001614 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001615 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001616 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001617 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001618 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001619 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001620 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001621 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001622
1623 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001624 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001625 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001626 auth.process_auth_options(self, options)
1627 user = self._process_swarming(options)
1628 if hasattr(options, 'user') and not options.user:
1629 options.user = user
1630 return options, args
1631
1632 def _process_swarming(self, options):
1633 """Processes the --swarming option and aborts if not specified.
1634
1635 Returns the identity as determined by the server.
1636 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001637 if not options.swarming:
1638 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001639 try:
1640 options.swarming = net.fix_url(options.swarming)
1641 except ValueError as e:
1642 self.error('--swarming %s' % e)
1643 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001644 try:
1645 user = auth.ensure_logged_in(options.swarming)
1646 except ValueError as e:
1647 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001648 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001649
1650
1651def main(args):
1652 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001653 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001654
1655
1656if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001657 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001658 fix_encoding.fix_encoding()
1659 tools.disable_buffering()
1660 colorama.init()
1661 sys.exit(main(sys.argv[1:]))