blob: 14bae4f9961a1712e1dab4c681a937f52908b994 [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
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10008__version__ = '0.9.3'
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
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +100016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import subprocess
18import sys
maruel11e31af2017-02-15 07:30:50 -080019import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070020import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000021import time
22import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000023
24from third_party import colorama
25from third_party.depot_tools import fix_encoding
26from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000027
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050028from utils import file_path
maruel12e30012015-10-09 11:55:35 -070029from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040030from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040031from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000032from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040033from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070034from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000035from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000036from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000037
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080038import auth
iannucci31ab9192017-05-02 19:11:56 -070039import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000040import isolateserver
maruelc070e672016-02-22 17:32:57 -080041import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000042
43
tansella4949442016-06-23 22:34:32 -070044ROOT_DIR = os.path.dirname(os.path.abspath(
45 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050046
47
48class Failure(Exception):
49 """Generic failure."""
50 pass
51
52
maruela9fe2cb2017-05-10 10:43:23 -070053def default_task_name(options):
54 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050055 if not options.task_name:
maruela9fe2cb2017-05-10 10:43:23 -070056 task_name = u'%s/%s' % (
maruel4e901792017-05-09 12:07:02 -070057 options.user,
maruel0165e822017-06-08 06:26:53 -070058 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruela9fe2cb2017-05-10 10:43:23 -070059 if options.isolated:
60 task_name += u'/' + options.isolated
61 return task_name
62 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050063
64
65### Triggering.
66
67
maruel77f720b2015-09-15 12:35:22 -070068# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070069CipdPackage = collections.namedtuple(
70 'CipdPackage',
71 [
72 'package_name',
73 'path',
74 'version',
75 ])
76
77
78# See ../appengine/swarming/swarming_rpcs.py.
79CipdInput = collections.namedtuple(
80 'CipdInput',
81 [
82 'client_package',
83 'packages',
84 'server',
85 ])
86
87
88# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070089FilesRef = collections.namedtuple(
90 'FilesRef',
91 [
92 'isolated',
93 'isolatedserver',
94 'namespace',
95 ])
96
97
98# See ../appengine/swarming/swarming_rpcs.py.
99TaskProperties = collections.namedtuple(
100 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500101 [
maruel681d6802017-01-17 16:56:03 -0800102 'caches',
borenet02f772b2016-06-22 12:42:19 -0700103 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500104 'command',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500105 'dimensions',
106 'env',
maruel77f720b2015-09-15 12:35:22 -0700107 'execution_timeout_secs',
108 'extra_args',
109 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700111 'inputs_ref',
112 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700113 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700114 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700115 ])
116
117
118# See ../appengine/swarming/swarming_rpcs.py.
119NewTaskRequest = collections.namedtuple(
120 'NewTaskRequest',
121 [
122 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500123 'name',
maruel77f720b2015-09-15 12:35:22 -0700124 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500125 'priority',
maruel77f720b2015-09-15 12:35:22 -0700126 'properties',
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700127 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500128 'tags',
129 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500130 ])
131
132
maruel77f720b2015-09-15 12:35:22 -0700133def namedtuple_to_dict(value):
134 """Recursively converts a namedtuple to a dict."""
135 out = dict(value._asdict())
136 for k, v in out.iteritems():
137 if hasattr(v, '_asdict'):
138 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700139 elif isinstance(v, (list, tuple)):
140 l = []
141 for elem in v:
142 if hasattr(elem, '_asdict'):
143 l.append(namedtuple_to_dict(elem))
144 else:
145 l.append(elem)
146 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700147 return out
148
149
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700150def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800151 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700152
153 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500154 """
maruel77f720b2015-09-15 12:35:22 -0700155 out = namedtuple_to_dict(task_request)
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700156 # Don't send 'service_account' if it is None to avoid confusing older
157 # version of the server that doesn't know about 'service_account' and don't
158 # use it at all.
159 if not out['service_account']:
160 out.pop('service_account')
maruel77f720b2015-09-15 12:35:22 -0700161 out['properties']['dimensions'] = [
162 {'key': k, 'value': v}
maruel0165e822017-06-08 06:26:53 -0700163 for k, v in out['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700164 ]
maruel77f720b2015-09-15 12:35:22 -0700165 out['properties']['env'] = [
166 {'key': k, 'value': v}
167 for k, v in out['properties']['env'].iteritems()
168 ]
169 out['properties']['env'].sort(key=lambda x: x['key'])
170 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171
172
maruel77f720b2015-09-15 12:35:22 -0700173def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500174 """Triggers a request on the Swarming server and returns the json data.
175
176 It's the low-level function.
177
178 Returns:
179 {
180 'request': {
181 'created_ts': u'2010-01-02 03:04:05',
182 'name': ..
183 },
184 'task_id': '12300',
185 }
186 """
187 logging.info('Triggering: %s', raw_request['name'])
188
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500189 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700190 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500191 if not result:
192 on_error.report('Failed to trigger task %s' % raw_request['name'])
193 return None
maruele557bce2015-11-17 09:01:27 -0800194 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800195 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800196 msg = 'Failed to trigger task %s' % raw_request['name']
197 if result['error'].get('errors'):
198 for err in result['error']['errors']:
199 if err.get('message'):
200 msg += '\nMessage: %s' % err['message']
201 if err.get('debugInfo'):
202 msg += '\nDebug info:\n%s' % err['debugInfo']
203 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800204 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800205
206 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800207 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500208 return result
209
210
211def setup_googletest(env, shards, index):
212 """Sets googletest specific environment variables."""
213 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700214 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
215 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
216 env = env[:]
217 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
218 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500219 return env
220
221
222def trigger_task_shards(swarming, task_request, shards):
223 """Triggers one or many subtasks of a sharded task.
224
225 Returns:
226 Dict with task details, returned to caller as part of --dump-json output.
227 None in case of failure.
228 """
229 def convert(index):
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700230 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500231 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700232 req['properties']['env'] = setup_googletest(
233 req['properties']['env'], shards, index)
234 req['name'] += ':%s:%s' % (index, shards)
235 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500236
237 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500238 tasks = {}
239 priority_warning = False
240 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700241 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500242 if not task:
243 break
244 logging.info('Request result: %s', task)
245 if (not priority_warning and
Marc-Antoine Ruel49ea2182017-08-17 10:07:49 -0400246 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500247 priority_warning = True
248 print >> sys.stderr, (
249 'Priority was reset to %s' % task['request']['priority'])
250 tasks[request['name']] = {
251 'shard_index': index,
252 'task_id': task['task_id'],
253 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
254 }
255
256 # Some shards weren't triggered. Abort everything.
257 if len(tasks) != len(requests):
258 if tasks:
259 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
260 len(tasks), len(requests))
261 for task_dict in tasks.itervalues():
262 abort_task(swarming, task_dict['task_id'])
263 return None
264
265 return tasks
266
267
268### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000269
270
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700271# How often to print status updates to stdout in 'collect'.
272STATUS_UPDATE_INTERVAL = 15 * 60.
273
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400274
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400275class State(object):
276 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000277
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400278 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
279 values are part of the API so if they change, the API changed.
280
281 It's in fact an enum. Values should be in decreasing order of importance.
282 """
283 RUNNING = 0x10
284 PENDING = 0x20
285 EXPIRED = 0x30
286 TIMED_OUT = 0x40
287 BOT_DIED = 0x50
288 CANCELED = 0x60
289 COMPLETED = 0x70
290
maruel77f720b2015-09-15 12:35:22 -0700291 STATES = (
292 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
293 'COMPLETED')
294 STATES_RUNNING = ('RUNNING', 'PENDING')
295 STATES_NOT_RUNNING = (
296 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
297 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
298 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400299
300 _NAMES = {
301 RUNNING: 'Running',
302 PENDING: 'Pending',
303 EXPIRED: 'Expired',
304 TIMED_OUT: 'Execution timed out',
305 BOT_DIED: 'Bot died',
306 CANCELED: 'User canceled',
307 COMPLETED: 'Completed',
308 }
309
maruel77f720b2015-09-15 12:35:22 -0700310 _ENUMS = {
311 'RUNNING': RUNNING,
312 'PENDING': PENDING,
313 'EXPIRED': EXPIRED,
314 'TIMED_OUT': TIMED_OUT,
315 'BOT_DIED': BOT_DIED,
316 'CANCELED': CANCELED,
317 'COMPLETED': COMPLETED,
318 }
319
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400320 @classmethod
321 def to_string(cls, state):
322 """Returns a user-readable string representing a State."""
323 if state not in cls._NAMES:
324 raise ValueError('Invalid state %s' % state)
325 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000326
maruel77f720b2015-09-15 12:35:22 -0700327 @classmethod
328 def from_enum(cls, state):
329 """Returns int value based on the string."""
330 if state not in cls._ENUMS:
331 raise ValueError('Invalid state %s' % state)
332 return cls._ENUMS[state]
333
maruel@chromium.org0437a732013-08-27 16:05:52 +0000334
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700335class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700336 """Assembles task execution summary (for --task-summary-json output).
337
338 Optionally fetches task outputs from isolate server to local disk (used when
339 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700340
341 This object is shared among multiple threads running 'retrieve_results'
342 function, in particular they call 'process_shard_result' method in parallel.
343 """
344
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000345 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700346 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
347
348 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700349 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700350 shard_count: expected number of task shards.
351 """
maruel12e30012015-10-09 11:55:35 -0700352 self.task_output_dir = (
353 unicode(os.path.abspath(task_output_dir))
354 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000355 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700356 self.shard_count = shard_count
357
358 self._lock = threading.Lock()
359 self._per_shard_results = {}
360 self._storage = None
361
nodire5028a92016-04-29 14:38:21 -0700362 if self.task_output_dir:
363 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364
Vadim Shtayurab450c602014-05-12 19:23:25 -0700365 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700366 """Stores results of a single task shard, fetches output files if necessary.
367
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400368 Modifies |result| in place.
369
maruel77f720b2015-09-15 12:35:22 -0700370 shard_index is 0-based.
371
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 Called concurrently from multiple threads.
373 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700375 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376 if shard_index < 0 or shard_index >= self.shard_count:
377 logging.warning(
378 'Shard index %d is outside of expected range: [0; %d]',
379 shard_index, self.shard_count - 1)
380 return
381
maruel77f720b2015-09-15 12:35:22 -0700382 if result.get('outputs_ref'):
383 ref = result['outputs_ref']
384 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
385 ref['isolatedserver'],
386 urllib.urlencode(
387 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400388
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700389 # Store result dict of that shard, ignore results we've already seen.
390 with self._lock:
391 if shard_index in self._per_shard_results:
392 logging.warning('Ignoring duplicate shard index %d', shard_index)
393 return
394 self._per_shard_results[shard_index] = result
395
396 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700397 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400398 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700399 result['outputs_ref']['isolatedserver'],
400 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400401 if storage:
402 # Output files are supposed to be small and they are not reused across
403 # tasks. So use MemoryCache for them instead of on-disk cache. Make
404 # files writable, so that calling script can delete them.
405 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700406 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400407 storage,
408 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700409 os.path.join(self.task_output_dir, str(shard_index)),
410 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700411
412 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700413 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700414 with self._lock:
415 # Write an array of shard results with None for missing shards.
416 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700417 'shards': [
418 self._per_shard_results.get(i) for i in xrange(self.shard_count)
419 ],
420 }
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000421
422 # Don't store stdout in the summary if not requested too.
423 if "json" not in self.task_output_stdout:
424 for shard_json in summary['shards']:
425 if not shard_json:
426 continue
427 if "output" in shard_json:
428 del shard_json["output"]
429 if "outputs" in shard_json:
430 del shard_json["outputs"]
431
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700432 # Write summary.json to task_output_dir as well.
433 if self.task_output_dir:
434 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700435 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700436 summary,
437 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700438 if self._storage:
439 self._storage.close()
440 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700441 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700442
443 def _get_storage(self, isolate_server, namespace):
444 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700445 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700446 with self._lock:
447 if not self._storage:
448 self._storage = isolateserver.get_storage(isolate_server, namespace)
449 else:
450 # Shards must all use exact same isolate server and namespace.
451 if self._storage.location != isolate_server:
452 logging.error(
453 'Task shards are using multiple isolate servers: %s and %s',
454 self._storage.location, isolate_server)
455 return None
456 if self._storage.namespace != namespace:
457 logging.error(
458 'Task shards are using multiple namespaces: %s and %s',
459 self._storage.namespace, namespace)
460 return None
461 return self._storage
462
463
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500464def now():
465 """Exists so it can be mocked easily."""
466 return time.time()
467
468
maruel77f720b2015-09-15 12:35:22 -0700469def parse_time(value):
470 """Converts serialized time from the API to datetime.datetime."""
471 # When microseconds are 0, the '.123456' suffix is elided. This means the
472 # serialized format is not consistent, which confuses the hell out of python.
473 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
474 try:
475 return datetime.datetime.strptime(value, fmt)
476 except ValueError:
477 pass
478 raise ValueError('Failed to parse %s' % value)
479
480
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700481def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700482 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000483 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400484 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700485
Vadim Shtayurab450c602014-05-12 19:23:25 -0700486 Returns:
487 <result dict> on success.
488 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700489 """
maruel71c61c82016-02-22 06:52:05 -0800490 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700491 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700492 if include_perf:
493 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700494 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700495 started = now()
496 deadline = started + timeout if timeout else None
497 attempt = 0
498
499 while not should_stop.is_set():
500 attempt += 1
501
502 # Waiting for too long -> give up.
503 current_time = now()
504 if deadline and current_time >= deadline:
505 logging.error('retrieve_results(%s) timed out on attempt %d',
506 base_url, attempt)
507 return None
508
509 # Do not spin too fast. Spin faster at the beginning though.
510 # Start with 1 sec delay and for each 30 sec of waiting add another second
511 # of delay, until hitting 15 sec ceiling.
512 if attempt > 1:
513 max_delay = min(15, 1 + (current_time - started) / 30.0)
514 delay = min(max_delay, deadline - current_time) if deadline else max_delay
515 if delay > 0:
516 logging.debug('Waiting %.1f sec before retrying', delay)
517 should_stop.wait(delay)
518 if should_stop.is_set():
519 return None
520
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400521 # Disable internal retries in net.url_read_json, since we are doing retries
522 # ourselves.
523 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700524 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
525 # request on GAE v2.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400526 result = net.url_read_json(result_url, retry_50x=False)
527 if not result:
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400528 continue
maruel77f720b2015-09-15 12:35:22 -0700529
maruelbf53e042015-12-01 15:00:51 -0800530 if result.get('error'):
531 # An error occurred.
532 if result['error'].get('errors'):
533 for err in result['error']['errors']:
534 logging.warning(
535 'Error while reading task: %s; %s',
536 err.get('message'), err.get('debugInfo'))
537 elif result['error'].get('message'):
538 logging.warning(
539 'Error while reading task: %s', result['error']['message'])
540 continue
541
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400542 if result['state'] in State.STATES_NOT_RUNNING:
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000543 if fetch_stdout:
544 out = net.url_read_json(output_url)
545 result['output'] = out.get('output') if out else out
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700546 # Record the result, try to fetch attached output files (if any).
547 if output_collector:
548 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700549 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700550 if result.get('internal_failure'):
551 logging.error('Internal error!')
552 elif result['state'] == 'BOT_DIED':
553 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700554 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000555
556
maruel77f720b2015-09-15 12:35:22 -0700557def convert_to_old_format(result):
558 """Converts the task result data from Endpoints API format to old API format
559 for compatibility.
560
561 This goes into the file generated as --task-summary-json.
562 """
563 # Sets default.
564 result.setdefault('abandoned_ts', None)
565 result.setdefault('bot_id', None)
566 result.setdefault('bot_version', None)
567 result.setdefault('children_task_ids', [])
568 result.setdefault('completed_ts', None)
569 result.setdefault('cost_saved_usd', None)
570 result.setdefault('costs_usd', None)
571 result.setdefault('deduped_from', None)
572 result.setdefault('name', None)
573 result.setdefault('outputs_ref', None)
574 result.setdefault('properties_hash', None)
575 result.setdefault('server_versions', None)
576 result.setdefault('started_ts', None)
577 result.setdefault('tags', None)
578 result.setdefault('user', None)
579
580 # Convertion back to old API.
581 duration = result.pop('duration', None)
582 result['durations'] = [duration] if duration else []
583 exit_code = result.pop('exit_code', None)
584 result['exit_codes'] = [int(exit_code)] if exit_code else []
585 result['id'] = result.pop('task_id')
586 result['isolated_out'] = result.get('outputs_ref', None)
587 output = result.pop('output', None)
588 result['outputs'] = [output] if output else []
589 # properties_hash
590 # server_version
591 # Endpoints result 'state' as string. For compatibility with old code, convert
592 # to int.
593 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700594 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700595 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700596 if 'bot_dimensions' in result:
597 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700598 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700599 }
600 else:
601 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700602
603
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700604def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400605 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000606 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500607 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000608
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700609 Duplicate shards are ignored. Shards are yielded in order of completion.
610 Timed out shards are NOT yielded at all. Caller can compare number of yielded
611 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000612
613 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500614 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 +0000615 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500616
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700617 output_collector is an optional instance of TaskOutputCollector that will be
618 used to fetch files produced by a task from isolate server to the local disk.
619
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500620 Yields:
621 (index, result). In particular, 'result' is defined as the
622 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000623 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000624 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400625 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700626 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700627 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700628
maruel@chromium.org0437a732013-08-27 16:05:52 +0000629 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
630 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700631 # Adds a task to the thread pool to call 'retrieve_results' and return
632 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400633 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700634 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000635 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400636 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000637 task_id, timeout, should_stop, output_collector, include_perf,
638 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700639
640 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400641 for shard_index, task_id in enumerate(task_ids):
642 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643
644 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400645 shards_remaining = range(len(task_ids))
646 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700647 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700648 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700649 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650 shard_index, result = results_channel.pull(
651 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700652 except threading_utils.TaskChannel.Timeout:
653 if print_status_updates:
654 print(
655 'Waiting for results from the following shards: %s' %
656 ', '.join(map(str, shards_remaining)))
657 sys.stdout.flush()
658 continue
659 except Exception:
660 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700661
662 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000664 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500665 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000666 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700667
Vadim Shtayurab450c602014-05-12 19:23:25 -0700668 # Yield back results to the caller.
669 assert shard_index in shards_remaining
670 shards_remaining.remove(shard_index)
671 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700672
maruel@chromium.org0437a732013-08-27 16:05:52 +0000673 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 should_stop.set()
676
677
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000678def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700680 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400681 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700682 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
683 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400684 else:
685 pending = 'N/A'
686
maruel77f720b2015-09-15 12:35:22 -0700687 if metadata.get('duration') is not None:
688 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400689 else:
690 duration = 'N/A'
691
maruel77f720b2015-09-15 12:35:22 -0700692 if metadata.get('exit_code') is not None:
693 # Integers are encoded as string to not loose precision.
694 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400695 else:
696 exit_code = 'N/A'
697
698 bot_id = metadata.get('bot_id') or 'N/A'
699
maruel77f720b2015-09-15 12:35:22 -0700700 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400701 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000702 tag_footer1 = 'End of shard %d' % (shard_index)
703 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
704 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400705
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000706 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
707 dash_pad = '+-%s-+' % ('-' * tag_len)
708 tag_header = '| %s |' % tag_header.ljust(tag_len)
709 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
710 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400711
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000712 if include_stdout:
713 return '\n'.join([
714 dash_pad,
715 tag_header,
716 dash_pad,
717 metadata.get('output', '').rstrip(),
718 dash_pad,
719 tag_footer1,
720 tag_footer2,
721 dash_pad,
722 ])
723 else:
724 return '\n'.join([
725 dash_pad,
726 tag_header,
727 tag_footer2,
728 dash_pad,
729 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000730
731
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700732def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700733 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000734 task_summary_json, task_output_dir, task_output_stdout,
735 include_perf):
maruela5490782015-09-30 10:56:59 -0700736 """Retrieves results of a Swarming task.
737
738 Returns:
739 process exit code that should be returned to the user.
740 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700741 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000742 output_collector = TaskOutputCollector(
743 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700744
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700745 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700746 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400747 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700748 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400749 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400750 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000751 output_collector, include_perf,
752 (len(task_output_stdout) > 0),
753 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700754 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700755
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400756 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700757 shard_exit_code = metadata.get('exit_code')
758 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700759 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700760 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700761 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400762 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700763 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700764
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700765 if decorate:
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000766 s = decorate_shard_output(
767 swarming, index, metadata,
768 "console" in task_output_stdout).encode(
769 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700770 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400771 if len(seen_shards) < len(task_ids):
772 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773 else:
maruel77f720b2015-09-15 12:35:22 -0700774 print('%s: %s %s' % (
775 metadata.get('bot_id', 'N/A'),
776 metadata['task_id'],
777 shard_exit_code))
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000778 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700779 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400780 if output:
781 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700782 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700783 summary = output_collector.finalize()
784 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700785 # TODO(maruel): Make this optional.
786 for i in summary['shards']:
787 if i:
788 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700789 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400791 if decorate and total_duration:
792 print('Total duration: %.1fs' % total_duration)
793
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400794 if len(seen_shards) != len(task_ids):
795 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700796 print >> sys.stderr, ('Results from some shards are missing: %s' %
797 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700798 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700799
maruela5490782015-09-30 10:56:59 -0700800 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000801
802
maruel77f720b2015-09-15 12:35:22 -0700803### API management.
804
805
806class APIError(Exception):
807 pass
808
809
810def endpoints_api_discovery_apis(host):
811 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
812 the APIs exposed by a host.
813
814 https://developers.google.com/discovery/v1/reference/apis/list
815 """
maruel380e3262016-08-31 16:10:06 -0700816 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
817 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700818 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
819 if data is None:
820 raise APIError('Failed to discover APIs on %s' % host)
821 out = {}
822 for api in data['items']:
823 if api['id'] == 'discovery:v1':
824 continue
825 # URL is of the following form:
826 # url = host + (
827 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
828 api_data = net.url_read_json(api['discoveryRestUrl'])
829 if api_data is None:
830 raise APIError('Failed to discover %s on %s' % (api['id'], host))
831 out[api['id']] = api_data
832 return out
833
834
maruel0165e822017-06-08 06:26:53 -0700835def get_yielder(base_url, limit):
836 """Returns the first query and a function that yields following items."""
837 CHUNK_SIZE = 250
838
839 url = base_url
840 if limit:
841 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
842 data = net.url_read_json(url)
843 if data is None:
844 # TODO(maruel): Do basic diagnostic.
845 raise Failure('Failed to access %s' % url)
846 org_cursor = data.pop('cursor', None)
847 org_total = len(data.get('items') or [])
848 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
849 if not org_cursor or not org_total:
850 # This is not an iterable resource.
851 return data, lambda: []
852
853 def yielder():
854 cursor = org_cursor
855 total = org_total
856 # Some items support cursors. Try to get automatically if cursors are needed
857 # by looking at the 'cursor' items.
858 while cursor and (not limit or total < limit):
859 merge_char = '&' if '?' in base_url else '?'
860 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
861 if limit:
862 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
863 new = net.url_read_json(url)
864 if new is None:
865 raise Failure('Failed to access %s' % url)
866 cursor = new.get('cursor')
867 new_items = new.get('items')
868 nb_items = len(new_items or [])
869 total += nb_items
870 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
871 yield new_items
872
873 return data, yielder
874
875
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500876### Commands.
877
878
879def abort_task(_swarming, _manifest):
880 """Given a task manifest that was triggered, aborts its execution."""
881 # TODO(vadimsh): No supported by the server yet.
882
883
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400884def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800885 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500886 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500887 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500888 dest='dimensions', metavar='FOO bar',
889 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500890 parser.add_option_group(parser.filter_group)
891
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400892
maruel0165e822017-06-08 06:26:53 -0700893def process_filter_options(parser, options):
894 for key, value in options.dimensions:
895 if ':' in key:
896 parser.error('--dimension key cannot contain ":"')
897 if key.strip() != key:
898 parser.error('--dimension key has whitespace')
899 if not key:
900 parser.error('--dimension key is empty')
901
902 if value.strip() != value:
903 parser.error('--dimension value has whitespace')
904 if not value:
905 parser.error('--dimension value is empty')
906 options.dimensions.sort()
907
908
Vadim Shtayurab450c602014-05-12 19:23:25 -0700909def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400910 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700911 parser.sharding_group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700912 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700913 help='Number of shards to trigger and collect.')
914 parser.add_option_group(parser.sharding_group)
915
916
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400917def add_trigger_options(parser):
918 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500919 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400920 add_filter_options(parser)
921
maruel681d6802017-01-17 16:56:03 -0800922 group = optparse.OptionGroup(parser, 'Task properties')
923 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700924 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500925 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800926 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500927 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700928 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800929 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400930 '--idempotent', action='store_true', default=False,
931 help='When set, the server will actively try to find a previous task '
932 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800933 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700934 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700935 help='The optional path to a file containing the secret_bytes to use with'
936 'this task.')
maruel681d6802017-01-17 16:56:03 -0800937 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700938 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400939 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800940 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700941 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400942 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800943 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500944 '--raw-cmd', action='store_true', default=False,
945 help='When set, the command after -- is used as-is without run_isolated. '
maruela9fe2cb2017-05-10 10:43:23 -0700946 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800947 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700948 '--cipd-package', action='append', default=[], metavar='PKG',
949 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700950 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800951 group.add_option(
952 '--named-cache', action='append', nargs=2, default=[],
maruel3773d8c2017-05-31 15:35:47 -0700953 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800954 help='"<name> <relpath>" items to keep a persistent bot managed cache')
955 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700956 '--service-account',
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700957 help='Email of a service account to run the task as, or literal "bot" '
958 'string to indicate that the task should use the same account the '
959 'bot itself is using to authenticate to Swarming. Don\'t use task '
960 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800961 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700962 '-o', '--output', action='append', default=[], metavar='PATH',
963 help='A list of files to return in addition to those written to '
964 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
965 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -0800966 parser.add_option_group(group)
967
968 group = optparse.OptionGroup(parser, 'Task request')
969 group.add_option(
970 '--priority', type='int', default=100,
971 help='The lower value, the more important the task is')
972 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700973 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -0800974 help='Display name of the task. Defaults to '
975 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
976 'isolated file is provided, if a hash is provided, it defaults to '
977 '<user>/<dimensions>/<isolated hash>/<timestamp>')
978 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700979 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -0800980 help='Tags to assign to the task.')
981 group.add_option(
982 '--user', default='',
983 help='User associated with the task. Defaults to authenticated user on '
984 'the server.')
985 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700986 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -0800987 help='Seconds to allow the task to be pending for a bot to run before '
988 'this task request expires.')
989 group.add_option(
990 '--deadline', type='int', dest='expiration',
991 help=optparse.SUPPRESS_HELP)
992 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +0000993
994
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500995def process_trigger_options(parser, options, args):
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700996 """Processes trigger options and does preparatory steps."""
maruel0165e822017-06-08 06:26:53 -0700997 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500998 options.env = dict(options.env)
maruela9fe2cb2017-05-10 10:43:23 -0700999 if args and args[0] == '--':
1000 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001001
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001002 if not options.dimensions:
1003 parser.error('Please at least specify one --dimension')
maruela9fe2cb2017-05-10 10:43:23 -07001004 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1005 parser.error('--tags must be in the format key:value')
1006 if options.raw_cmd and not args:
1007 parser.error(
1008 'Arguments with --raw-cmd should be passed after -- as command '
1009 'delimiter.')
1010 if options.isolate_server and not options.namespace:
1011 parser.error(
1012 '--namespace must be a valid value when --isolate-server is used')
1013 if not options.isolated and not options.raw_cmd:
1014 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1015
1016 # Isolated
1017 # --isolated is required only if --raw-cmd wasn't provided.
1018 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1019 # preferred server.
1020 isolateserver.process_isolate_server_options(
1021 parser, options, False, not options.raw_cmd)
1022 inputs_ref = None
1023 if options.isolate_server:
1024 inputs_ref = FilesRef(
1025 isolated=options.isolated,
1026 isolatedserver=options.isolate_server,
1027 namespace=options.namespace)
1028
1029 # Command
1030 command = None
1031 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001032 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001033 command = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001034 else:
maruela9fe2cb2017-05-10 10:43:23 -07001035 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001036
maruela9fe2cb2017-05-10 10:43:23 -07001037 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001038 cipd_packages = []
1039 for p in options.cipd_package:
1040 split = p.split(':', 2)
1041 if len(split) != 3:
1042 parser.error('CIPD packages must take the form: path:package:version')
1043 cipd_packages.append(CipdPackage(
1044 package_name=split[1],
1045 path=split[0],
1046 version=split[2]))
1047 cipd_input = None
1048 if cipd_packages:
1049 cipd_input = CipdInput(
1050 client_package=None,
1051 packages=cipd_packages,
1052 server=None)
1053
maruela9fe2cb2017-05-10 10:43:23 -07001054 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001055 secret_bytes = None
1056 if options.secret_bytes_path:
1057 with open(options.secret_bytes_path, 'r') as f:
1058 secret_bytes = f.read().encode('base64')
1059
maruela9fe2cb2017-05-10 10:43:23 -07001060 # Named caches
maruel681d6802017-01-17 16:56:03 -08001061 caches = [
1062 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1063 for i in options.named_cache
1064 ]
maruela9fe2cb2017-05-10 10:43:23 -07001065
maruel77f720b2015-09-15 12:35:22 -07001066 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001067 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001068 cipd_input=cipd_input,
maruela9fe2cb2017-05-10 10:43:23 -07001069 command=command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001070 dimensions=options.dimensions,
1071 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001072 execution_timeout_secs=options.hard_timeout,
maruela9fe2cb2017-05-10 10:43:23 -07001073 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001074 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001075 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001076 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001077 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001078 outputs=options.output,
1079 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001080
maruel77f720b2015-09-15 12:35:22 -07001081 return NewTaskRequest(
1082 expiration_secs=options.expiration,
maruela9fe2cb2017-05-10 10:43:23 -07001083 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001084 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001085 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001086 properties=properties,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001087 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001088 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001089 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001090
1091
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001092class TaskOutputStdoutOption(optparse.Option):
1093 """Where to output the each task's console output (stderr/stdout).
1094
1095 The output will be;
1096 none - not be downloaded.
1097 json - stored in summary.json file *only*.
1098 console - shown on stdout *only*.
1099 all - stored in summary.json and shown on stdout.
1100 """
1101
1102 choices = ['all', 'json', 'console', 'none']
1103
1104 def __init__(self, *args, **kw):
1105 optparse.Option.__init__(
1106 self,
1107 *args,
1108 choices=self.choices,
1109 help=re.sub('\s\s*', ' ', self.__doc__),
1110 **kw)
1111
1112 def convert_value(self, opt, value):
1113 if value not in self.choices:
1114 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1115 self.get_opt_string(), self.choices, value))
1116 stdout_to = []
1117 if value == 'all':
1118 stdout_to = ['json', 'console']
1119 elif value != 'none':
1120 stdout_to = [value]
1121 return stdout_to
1122
1123
maruel@chromium.org0437a732013-08-27 16:05:52 +00001124def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001125 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001126 '-t', '--timeout', type='float',
1127 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1128 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001129 parser.group_logging.add_option(
1130 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001131 parser.group_logging.add_option(
1132 '--print-status-updates', action='store_true',
1133 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001134 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001135 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001136 '--task-summary-json',
1137 metavar='FILE',
1138 help='Dump a summary of task results to this file as json. It contains '
1139 'only shards statuses as know to server directly. Any output files '
1140 'emitted by the task can be collected by using --task-output-dir')
1141 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001142 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001143 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001144 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001145 'directory contains per-shard directory with output files produced '
1146 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001147 parser.task_output_group.add_option(TaskOutputStdoutOption(
1148 '--task-output-stdout', default='all'))
maruel9531ce02016-04-13 06:11:23 -07001149 parser.task_output_group.add_option(
1150 '--perf', action='store_true', default=False,
1151 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001152 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001153
1154
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001155@subcommand.usage('bots...')
1156def CMDbot_delete(parser, args):
1157 """Forcibly deletes bots from the Swarming server."""
1158 parser.add_option(
1159 '-f', '--force', action='store_true',
1160 help='Do not prompt for confirmation')
1161 options, args = parser.parse_args(args)
1162 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001163 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001164
1165 bots = sorted(args)
1166 if not options.force:
1167 print('Delete the following bots?')
1168 for bot in bots:
1169 print(' %s' % bot)
1170 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1171 print('Goodbye.')
1172 return 1
1173
1174 result = 0
1175 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001176 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001177 if net.url_read_json(url, data={}, method='POST') is None:
1178 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001179 result = 1
1180 return result
1181
1182
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001183def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001184 """Returns information about the bots connected to the Swarming server."""
1185 add_filter_options(parser)
1186 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001187 '--dead-only', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001188 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001189 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001190 '-k', '--keep-dead', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001191 help='Keep both dead and alive bots')
1192 parser.filter_group.add_option(
1193 '--busy', action='store_true', help='Keep only busy bots')
1194 parser.filter_group.add_option(
1195 '--idle', action='store_true', help='Keep only idle bots')
1196 parser.filter_group.add_option(
1197 '--mp', action='store_true',
1198 help='Keep only Machine Provider managed bots')
1199 parser.filter_group.add_option(
1200 '--non-mp', action='store_true',
1201 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001202 parser.filter_group.add_option(
1203 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001204 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001205 options, args = parser.parse_args(args)
maruel0165e822017-06-08 06:26:53 -07001206 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001207
1208 if options.keep_dead and options.dead_only:
maruel0165e822017-06-08 06:26:53 -07001209 parser.error('Use only one of --keep-dead or --dead-only')
1210 if options.busy and options.idle:
1211 parser.error('Use only one of --busy or --idle')
1212 if options.mp and options.non_mp:
1213 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001214
maruel0165e822017-06-08 06:26:53 -07001215 url = options.swarming + '/api/swarming/v1/bots/list?'
1216 values = []
1217 if options.dead_only:
1218 values.append(('is_dead', 'TRUE'))
1219 elif options.keep_dead:
1220 values.append(('is_dead', 'NONE'))
1221 else:
1222 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001223
maruel0165e822017-06-08 06:26:53 -07001224 if options.busy:
1225 values.append(('is_busy', 'TRUE'))
1226 elif options.idle:
1227 values.append(('is_busy', 'FALSE'))
1228 else:
1229 values.append(('is_busy', 'NONE'))
1230
1231 if options.mp:
1232 values.append(('is_mp', 'TRUE'))
1233 elif options.non_mp:
1234 values.append(('is_mp', 'FALSE'))
1235 else:
1236 values.append(('is_mp', 'NONE'))
1237
1238 for key, value in options.dimensions:
1239 values.append(('dimensions', '%s:%s' % (key, value)))
1240 url += urllib.urlencode(values)
1241 try:
1242 data, yielder = get_yielder(url, 0)
1243 bots = data.get('items') or []
1244 for items in yielder():
1245 if items:
1246 bots.extend(items)
1247 except Failure as e:
1248 sys.stderr.write('\n%s\n' % e)
1249 return 1
maruel77f720b2015-09-15 12:35:22 -07001250 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruel0165e822017-06-08 06:26:53 -07001251 print bot['bot_id']
1252 if not options.bare:
1253 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1254 print ' %s' % json.dumps(dimensions, sort_keys=True)
1255 if bot.get('task_id'):
1256 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001257 return 0
1258
1259
maruelfd0a90c2016-06-10 11:51:10 -07001260@subcommand.usage('task_id')
1261def CMDcancel(parser, args):
1262 """Cancels a task."""
1263 options, args = parser.parse_args(args)
1264 if not args:
1265 parser.error('Please specify the task to cancel')
1266 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001267 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001268 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1269 print('Deleting %s failed. Probably already gone' % task_id)
1270 return 1
1271 return 0
1272
1273
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001274@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001275def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001276 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001277
1278 The result can be in multiple part if the execution was sharded. It can
1279 potentially have retries.
1280 """
1281 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001282 parser.add_option(
1283 '-j', '--json',
1284 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001285 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001286 if not args and not options.json:
1287 parser.error('Must specify at least one task id or --json.')
1288 if args and options.json:
1289 parser.error('Only use one of task id or --json.')
1290
1291 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001292 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001293 try:
maruel1ceb3872015-10-14 06:10:44 -07001294 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001295 data = json.load(f)
1296 except (IOError, ValueError):
1297 parser.error('Failed to open %s' % options.json)
1298 try:
1299 tasks = sorted(
1300 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1301 args = [t['task_id'] for t in tasks]
1302 except (KeyError, TypeError):
1303 parser.error('Failed to process %s' % options.json)
1304 if options.timeout is None:
1305 options.timeout = (
1306 data['request']['properties']['execution_timeout_secs'] +
1307 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001308 else:
1309 valid = frozenset('0123456789abcdef')
1310 if any(not valid.issuperset(task_id) for task_id in args):
1311 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001312
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001313 try:
1314 return collect(
1315 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001316 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001317 options.timeout,
1318 options.decorate,
1319 options.print_status_updates,
1320 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001321 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001322 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001323 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001324 except Failure:
1325 on_error.report(None)
1326 return 1
1327
1328
maruelbea00862015-09-18 09:55:36 -07001329@subcommand.usage('[filename]')
1330def CMDput_bootstrap(parser, args):
1331 """Uploads a new version of bootstrap.py."""
1332 options, args = parser.parse_args(args)
1333 if len(args) != 1:
1334 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001335 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001336 path = unicode(os.path.abspath(args[0]))
1337 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001338 content = f.read().decode('utf-8')
1339 data = net.url_read_json(url, data={'content': content})
1340 print data
1341 return 0
1342
1343
1344@subcommand.usage('[filename]')
1345def CMDput_bot_config(parser, args):
1346 """Uploads a new version of bot_config.py."""
1347 options, args = parser.parse_args(args)
1348 if len(args) != 1:
1349 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001350 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001351 path = unicode(os.path.abspath(args[0]))
1352 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001353 content = f.read().decode('utf-8')
1354 data = net.url_read_json(url, data={'content': content})
1355 print data
1356 return 0
1357
1358
maruel77f720b2015-09-15 12:35:22 -07001359@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001360def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001361 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1362 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001363
1364 Examples:
maruel0165e822017-06-08 06:26:53 -07001365 Raw task request and results:
1366 swarming.py query -S server-url.com task/123456/request
1367 swarming.py query -S server-url.com task/123456/result
1368
maruel77f720b2015-09-15 12:35:22 -07001369 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001370 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001371
maruel0165e822017-06-08 06:26:53 -07001372 Listing last 10 tasks on a specific bot named 'bot1':
1373 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001374
maruel0165e822017-06-08 06:26:53 -07001375 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001376 quoting is important!:
1377 swarming.py query -S server-url.com --limit 10 \\
maruel0165e822017-06-08 06:26:53 -07001378 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001379 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001380 parser.add_option(
1381 '-L', '--limit', type='int', default=200,
1382 help='Limit to enforce on limitless items (like number of tasks); '
1383 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001384 parser.add_option(
1385 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001386 parser.add_option(
1387 '--progress', action='store_true',
1388 help='Prints a dot at each request to show progress')
1389 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001390 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001391 parser.error(
1392 'Must specify only method name and optionally query args properly '
1393 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001394 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruel0165e822017-06-08 06:26:53 -07001395 try:
1396 data, yielder = get_yielder(base_url, options.limit)
1397 for items in yielder():
1398 if items:
1399 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001400 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001401 sys.stderr.write('.')
1402 sys.stderr.flush()
1403 except Failure as e:
1404 sys.stderr.write('\n%s\n' % e)
1405 return 1
maruel77f720b2015-09-15 12:35:22 -07001406 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001407 sys.stderr.write('\n')
1408 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001409 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001410 options.json = unicode(os.path.abspath(options.json))
1411 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001412 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001413 try:
maruel77f720b2015-09-15 12:35:22 -07001414 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001415 sys.stdout.write('\n')
1416 except IOError:
1417 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001418 return 0
1419
1420
maruel77f720b2015-09-15 12:35:22 -07001421def CMDquery_list(parser, args):
1422 """Returns list of all the Swarming APIs that can be used with command
1423 'query'.
1424 """
1425 parser.add_option(
1426 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1427 options, args = parser.parse_args(args)
1428 if args:
1429 parser.error('No argument allowed.')
1430
1431 try:
1432 apis = endpoints_api_discovery_apis(options.swarming)
1433 except APIError as e:
1434 parser.error(str(e))
1435 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001436 options.json = unicode(os.path.abspath(options.json))
1437 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001438 json.dump(apis, f)
1439 else:
1440 help_url = (
1441 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1442 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001443 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1444 if i:
1445 print('')
maruel77f720b2015-09-15 12:35:22 -07001446 print api_id
maruel11e31af2017-02-15 07:30:50 -08001447 print ' ' + api['description'].strip()
1448 if 'resources' in api:
1449 # Old.
1450 for j, (resource_name, resource) in enumerate(
1451 sorted(api['resources'].iteritems())):
1452 if j:
1453 print('')
1454 for method_name, method in sorted(resource['methods'].iteritems()):
1455 # Only list the GET ones.
1456 if method['httpMethod'] != 'GET':
1457 continue
1458 print '- %s.%s: %s' % (
1459 resource_name, method_name, method['path'])
1460 print('\n'.join(
1461 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1462 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1463 else:
1464 # New.
1465 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001466 # Only list the GET ones.
1467 if method['httpMethod'] != 'GET':
1468 continue
maruel11e31af2017-02-15 07:30:50 -08001469 print '- %s: %s' % (method['id'], method['path'])
1470 print('\n'.join(
1471 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001472 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1473 return 0
1474
1475
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001476@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001477def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001478 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001479
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001480 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001481 """
1482 add_trigger_options(parser)
1483 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001484 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001485 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001486 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001487 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001488 tasks = trigger_task_shards(
1489 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001490 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001491 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001492 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001493 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001494 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001495 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001496 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001497 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001498 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001499 task_ids = [
1500 t['task_id']
1501 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1502 ]
maruel71c61c82016-02-22 06:52:05 -08001503 if options.timeout is None:
1504 options.timeout = (
1505 task_request.properties.execution_timeout_secs +
1506 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001507 try:
1508 return collect(
1509 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001510 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001511 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001512 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001513 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001514 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001515 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001516 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001517 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001518 except Failure:
1519 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001520 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001521
1522
maruel18122c62015-10-23 06:31:23 -07001523@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001524def CMDreproduce(parser, args):
1525 """Runs a task locally that was triggered on the server.
1526
1527 This running locally the same commands that have been run on the bot. The data
1528 downloaded will be in a subdirectory named 'work' of the current working
1529 directory.
maruel18122c62015-10-23 06:31:23 -07001530
1531 You can pass further additional arguments to the target command by passing
1532 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001533 """
maruelc070e672016-02-22 17:32:57 -08001534 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001535 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001536 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001537 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001538 extra_args = []
1539 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001540 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001541 if len(args) > 1:
1542 if args[1] == '--':
1543 if len(args) > 2:
1544 extra_args = args[2:]
1545 else:
1546 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001547
maruel380e3262016-08-31 16:10:06 -07001548 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001549 request = net.url_read_json(url)
1550 if not request:
1551 print >> sys.stderr, 'Failed to retrieve request data for the task'
1552 return 1
1553
maruel12e30012015-10-09 11:55:35 -07001554 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001555 if fs.isdir(workdir):
1556 parser.error('Please delete the directory \'work\' first')
1557 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001558 cachedir = unicode(os.path.abspath('cipd_cache'))
1559 if not fs.exists(cachedir):
1560 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001561
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001562 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001563 env = os.environ.copy()
1564 env['SWARMING_BOT_ID'] = 'reproduce'
1565 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001566 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001567 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001568 for i in properties['env']:
1569 key = i['key'].encode('utf-8')
1570 if not i['value']:
1571 env.pop(key, None)
1572 else:
1573 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001574
iannucci31ab9192017-05-02 19:11:56 -07001575 command = []
nodir152cba62016-05-12 16:08:56 -07001576 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001577 # Create the tree.
1578 with isolateserver.get_storage(
1579 properties['inputs_ref']['isolatedserver'],
1580 properties['inputs_ref']['namespace']) as storage:
1581 bundle = isolateserver.fetch_isolated(
1582 properties['inputs_ref']['isolated'],
1583 storage,
1584 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001585 workdir,
1586 False)
maruel29ab2fd2015-10-16 11:44:01 -07001587 command = bundle.command
1588 if bundle.relative_cwd:
1589 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001590 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001591
1592 if properties.get('command'):
1593 command.extend(properties['command'])
1594
1595 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1596 new_command = tools.fix_python_path(command)
1597 new_command = run_isolated.process_command(
1598 new_command, options.output_dir, None)
1599 if not options.output_dir and new_command != command:
1600 parser.error('The task has outputs, you must use --output-dir')
1601 command = new_command
1602 file_path.ensure_command_has_abs_path(command, workdir)
1603
1604 if properties.get('cipd_input'):
1605 ci = properties['cipd_input']
1606 cp = ci['client_package']
1607 client_manager = cipd.get_client(
1608 ci['server'], cp['package_name'], cp['version'], cachedir)
1609
1610 with client_manager as client:
1611 by_path = collections.defaultdict(list)
1612 for pkg in ci['packages']:
1613 path = pkg['path']
1614 # cipd deals with 'root' as ''
1615 if path == '.':
1616 path = ''
1617 by_path[path].append((pkg['package_name'], pkg['version']))
1618 client.ensure(workdir, by_path, cache_dir=cachedir)
1619
maruel77f720b2015-09-15 12:35:22 -07001620 try:
maruel18122c62015-10-23 06:31:23 -07001621 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001622 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001623 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001624 print >> sys.stderr, str(e)
1625 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001626
1627
maruel0eb1d1b2015-10-02 14:48:21 -07001628@subcommand.usage('bot_id')
1629def CMDterminate(parser, args):
1630 """Tells a bot to gracefully shut itself down as soon as it can.
1631
1632 This is done by completing whatever current task there is then exiting the bot
1633 process.
1634 """
1635 parser.add_option(
1636 '--wait', action='store_true', help='Wait for the bot to terminate')
1637 options, args = parser.parse_args(args)
1638 if len(args) != 1:
1639 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001640 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001641 request = net.url_read_json(url, data={})
1642 if not request:
1643 print >> sys.stderr, 'Failed to ask for termination'
1644 return 1
1645 if options.wait:
1646 return collect(
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001647 options.swarming,
1648 [request['task_id']],
1649 0.,
1650 False,
1651 False,
1652 None,
1653 None,
1654 [],
maruel9531ce02016-04-13 06:11:23 -07001655 False)
maruelb7ded002017-06-10 16:43:17 -07001656 else:
1657 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001658 return 0
1659
1660
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001661@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001662def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001663 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001664
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001665 Passes all extra arguments provided after '--' as additional command line
1666 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001667 """
1668 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001669 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001670 parser.add_option(
1671 '--dump-json',
1672 metavar='FILE',
1673 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001674 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001675 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001676 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001677 tasks = trigger_task_shards(
1678 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001679 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001680 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001681 tasks_sorted = sorted(
1682 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001683 if options.dump_json:
1684 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001685 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001686 'tasks': tasks,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001687 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001688 }
maruel46b015f2015-10-13 18:40:35 -07001689 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001690 print('To collect results, use:')
1691 print(' swarming.py collect -S %s --json %s' %
1692 (options.swarming, options.dump_json))
1693 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001694 print('To collect results, use:')
1695 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001696 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1697 print('Or visit:')
1698 for t in tasks_sorted:
1699 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001700 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001701 except Failure:
1702 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001703 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001704
1705
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001706class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001707 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001708 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001709 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001710 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001711 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001712 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001713 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001714 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001715 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001716 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001717
1718 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001719 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001720 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001721 auth.process_auth_options(self, options)
1722 user = self._process_swarming(options)
1723 if hasattr(options, 'user') and not options.user:
1724 options.user = user
1725 return options, args
1726
1727 def _process_swarming(self, options):
1728 """Processes the --swarming option and aborts if not specified.
1729
1730 Returns the identity as determined by the server.
1731 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001732 if not options.swarming:
1733 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001734 try:
1735 options.swarming = net.fix_url(options.swarming)
1736 except ValueError as e:
1737 self.error('--swarming %s' % e)
1738 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001739 try:
1740 user = auth.ensure_logged_in(options.swarming)
1741 except ValueError as e:
1742 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001743 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001744
1745
1746def main(args):
1747 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001748 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001749
1750
1751if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001752 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001753 fix_encoding.fix_encoding()
1754 tools.disable_buffering()
1755 colorama.init()
1756 sys.exit(main(sys.argv[1:]))