blob: fa50fa134026522f7a395577df39d462db5f2a78 [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)
Vadim Shtayura96eff0d2017-11-03 15:32:51 -0700545 result['output'] = out.get('output', '') if out else ''
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 Ruel6bb4fb32017-11-03 14:34:49 -0400684 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
685 metadata.get('abandoned_ts')):
686 pending = '%.1fs' % (
687 parse_time(metadata['abandoned_ts']) -
688 parse_time(metadata['created_ts'])
689 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400690 else:
691 pending = 'N/A'
692
maruel77f720b2015-09-15 12:35:22 -0700693 if metadata.get('duration') is not None:
694 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400695 else:
696 duration = 'N/A'
697
maruel77f720b2015-09-15 12:35:22 -0700698 if metadata.get('exit_code') is not None:
699 # Integers are encoded as string to not loose precision.
700 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400701 else:
702 exit_code = 'N/A'
703
704 bot_id = metadata.get('bot_id') or 'N/A'
705
maruel77f720b2015-09-15 12:35:22 -0700706 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400707 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000708 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel6bb4fb32017-11-03 14:34:49 -0400709 if metadata.get('state') == 'CANCELED':
710 tag_footer2 = ' Pending: %s CANCELED' % pending
711 elif metadata.get('state') == 'EXPIRED':
712 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
713 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT'):
714 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
715 pending, duration, bot_id, exit_code, metadata['state'])
716 else:
717 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
718 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400719
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000720 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
721 dash_pad = '+-%s-+' % ('-' * tag_len)
722 tag_header = '| %s |' % tag_header.ljust(tag_len)
723 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
724 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400725
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000726 if include_stdout:
727 return '\n'.join([
728 dash_pad,
729 tag_header,
730 dash_pad,
Marc-Antoine Ruel6bb4fb32017-11-03 14:34:49 -0400731 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000732 dash_pad,
733 tag_footer1,
734 tag_footer2,
735 dash_pad,
736 ])
737 else:
738 return '\n'.join([
739 dash_pad,
740 tag_header,
741 tag_footer2,
742 dash_pad,
743 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000744
745
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700746def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700747 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000748 task_summary_json, task_output_dir, task_output_stdout,
749 include_perf):
maruela5490782015-09-30 10:56:59 -0700750 """Retrieves results of a Swarming task.
751
752 Returns:
753 process exit code that should be returned to the user.
754 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700755 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000756 output_collector = TaskOutputCollector(
757 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700758
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700759 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700760 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400761 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700762 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400763 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400764 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000765 output_collector, include_perf,
766 (len(task_output_stdout) > 0),
767 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700768 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700769
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400770 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700771 shard_exit_code = metadata.get('exit_code')
772 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700773 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700774 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700775 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400776 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700777 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700778
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700779 if decorate:
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000780 s = decorate_shard_output(
781 swarming, index, metadata,
782 "console" in task_output_stdout).encode(
783 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700784 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400785 if len(seen_shards) < len(task_ids):
786 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700787 else:
maruel77f720b2015-09-15 12:35:22 -0700788 print('%s: %s %s' % (
789 metadata.get('bot_id', 'N/A'),
790 metadata['task_id'],
791 shard_exit_code))
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +1000792 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700793 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400794 if output:
795 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700796 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700797 summary = output_collector.finalize()
798 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700799 # TODO(maruel): Make this optional.
800 for i in summary['shards']:
801 if i:
802 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700803 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700804
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400805 if decorate and total_duration:
806 print('Total duration: %.1fs' % total_duration)
807
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400808 if len(seen_shards) != len(task_ids):
809 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700810 print >> sys.stderr, ('Results from some shards are missing: %s' %
811 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700812 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700813
maruela5490782015-09-30 10:56:59 -0700814 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000815
816
maruel77f720b2015-09-15 12:35:22 -0700817### API management.
818
819
820class APIError(Exception):
821 pass
822
823
824def endpoints_api_discovery_apis(host):
825 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
826 the APIs exposed by a host.
827
828 https://developers.google.com/discovery/v1/reference/apis/list
829 """
maruel380e3262016-08-31 16:10:06 -0700830 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
831 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700832 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
833 if data is None:
834 raise APIError('Failed to discover APIs on %s' % host)
835 out = {}
836 for api in data['items']:
837 if api['id'] == 'discovery:v1':
838 continue
839 # URL is of the following form:
840 # url = host + (
841 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
842 api_data = net.url_read_json(api['discoveryRestUrl'])
843 if api_data is None:
844 raise APIError('Failed to discover %s on %s' % (api['id'], host))
845 out[api['id']] = api_data
846 return out
847
848
maruel0165e822017-06-08 06:26:53 -0700849def get_yielder(base_url, limit):
850 """Returns the first query and a function that yields following items."""
851 CHUNK_SIZE = 250
852
853 url = base_url
854 if limit:
855 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
856 data = net.url_read_json(url)
857 if data is None:
858 # TODO(maruel): Do basic diagnostic.
859 raise Failure('Failed to access %s' % url)
860 org_cursor = data.pop('cursor', None)
861 org_total = len(data.get('items') or [])
862 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
863 if not org_cursor or not org_total:
864 # This is not an iterable resource.
865 return data, lambda: []
866
867 def yielder():
868 cursor = org_cursor
869 total = org_total
870 # Some items support cursors. Try to get automatically if cursors are needed
871 # by looking at the 'cursor' items.
872 while cursor and (not limit or total < limit):
873 merge_char = '&' if '?' in base_url else '?'
874 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
875 if limit:
876 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
877 new = net.url_read_json(url)
878 if new is None:
879 raise Failure('Failed to access %s' % url)
880 cursor = new.get('cursor')
881 new_items = new.get('items')
882 nb_items = len(new_items or [])
883 total += nb_items
884 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
885 yield new_items
886
887 return data, yielder
888
889
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500890### Commands.
891
892
893def abort_task(_swarming, _manifest):
894 """Given a task manifest that was triggered, aborts its execution."""
895 # TODO(vadimsh): No supported by the server yet.
896
897
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400898def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800899 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500900 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500901 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500902 dest='dimensions', metavar='FOO bar',
903 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500904 parser.add_option_group(parser.filter_group)
905
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400906
maruel0165e822017-06-08 06:26:53 -0700907def process_filter_options(parser, options):
908 for key, value in options.dimensions:
909 if ':' in key:
910 parser.error('--dimension key cannot contain ":"')
911 if key.strip() != key:
912 parser.error('--dimension key has whitespace')
913 if not key:
914 parser.error('--dimension key is empty')
915
916 if value.strip() != value:
917 parser.error('--dimension value has whitespace')
918 if not value:
919 parser.error('--dimension value is empty')
920 options.dimensions.sort()
921
922
Vadim Shtayurab450c602014-05-12 19:23:25 -0700923def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400924 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700925 parser.sharding_group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700926 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700927 help='Number of shards to trigger and collect.')
928 parser.add_option_group(parser.sharding_group)
929
930
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400931def add_trigger_options(parser):
932 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500933 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400934 add_filter_options(parser)
935
maruel681d6802017-01-17 16:56:03 -0800936 group = optparse.OptionGroup(parser, 'Task properties')
937 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700938 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500939 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800940 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500941 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700942 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800943 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400944 '--idempotent', action='store_true', default=False,
945 help='When set, the server will actively try to find a previous task '
946 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800947 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700948 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700949 help='The optional path to a file containing the secret_bytes to use with'
950 'this task.')
maruel681d6802017-01-17 16:56:03 -0800951 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700952 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400953 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800954 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700955 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400956 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800957 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500958 '--raw-cmd', action='store_true', default=False,
959 help='When set, the command after -- is used as-is without run_isolated. '
maruela9fe2cb2017-05-10 10:43:23 -0700960 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800961 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700962 '--cipd-package', action='append', default=[], metavar='PKG',
963 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700964 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800965 group.add_option(
966 '--named-cache', action='append', nargs=2, default=[],
maruel3773d8c2017-05-31 15:35:47 -0700967 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800968 help='"<name> <relpath>" items to keep a persistent bot managed cache')
969 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700970 '--service-account',
Vadim Shtayura9aef3f12017-08-14 17:41:24 -0700971 help='Email of a service account to run the task as, or literal "bot" '
972 'string to indicate that the task should use the same account the '
973 'bot itself is using to authenticate to Swarming. Don\'t use task '
974 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800975 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700976 '-o', '--output', action='append', default=[], metavar='PATH',
977 help='A list of files to return in addition to those written to '
978 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
979 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -0800980 parser.add_option_group(group)
981
982 group = optparse.OptionGroup(parser, 'Task request')
983 group.add_option(
984 '--priority', type='int', default=100,
985 help='The lower value, the more important the task is')
986 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700987 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -0800988 help='Display name of the task. Defaults to '
989 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
990 'isolated file is provided, if a hash is provided, it defaults to '
991 '<user>/<dimensions>/<isolated hash>/<timestamp>')
992 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -0700993 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -0800994 help='Tags to assign to the task.')
995 group.add_option(
996 '--user', default='',
997 help='User associated with the task. Defaults to authenticated user on '
998 'the server.')
999 group.add_option(
maruel3773d8c2017-05-31 15:35:47 -07001000 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001001 help='Seconds to allow the task to be pending for a bot to run before '
1002 'this task request expires.')
1003 group.add_option(
1004 '--deadline', type='int', dest='expiration',
1005 help=optparse.SUPPRESS_HELP)
1006 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001007
1008
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001009def process_trigger_options(parser, options, args):
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001010 """Processes trigger options and does preparatory steps."""
maruel0165e822017-06-08 06:26:53 -07001011 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001012 options.env = dict(options.env)
maruela9fe2cb2017-05-10 10:43:23 -07001013 if args and args[0] == '--':
1014 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001015
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001016 if not options.dimensions:
1017 parser.error('Please at least specify one --dimension')
maruela9fe2cb2017-05-10 10:43:23 -07001018 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1019 parser.error('--tags must be in the format key:value')
1020 if options.raw_cmd and not args:
1021 parser.error(
1022 'Arguments with --raw-cmd should be passed after -- as command '
1023 'delimiter.')
1024 if options.isolate_server and not options.namespace:
1025 parser.error(
1026 '--namespace must be a valid value when --isolate-server is used')
1027 if not options.isolated and not options.raw_cmd:
1028 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1029
1030 # Isolated
1031 # --isolated is required only if --raw-cmd wasn't provided.
1032 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1033 # preferred server.
1034 isolateserver.process_isolate_server_options(
1035 parser, options, False, not options.raw_cmd)
1036 inputs_ref = None
1037 if options.isolate_server:
1038 inputs_ref = FilesRef(
1039 isolated=options.isolated,
1040 isolatedserver=options.isolate_server,
1041 namespace=options.namespace)
1042
1043 # Command
1044 command = None
1045 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001046 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001047 command = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001048 else:
maruela9fe2cb2017-05-10 10:43:23 -07001049 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001050
maruela9fe2cb2017-05-10 10:43:23 -07001051 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001052 cipd_packages = []
1053 for p in options.cipd_package:
1054 split = p.split(':', 2)
1055 if len(split) != 3:
1056 parser.error('CIPD packages must take the form: path:package:version')
1057 cipd_packages.append(CipdPackage(
1058 package_name=split[1],
1059 path=split[0],
1060 version=split[2]))
1061 cipd_input = None
1062 if cipd_packages:
1063 cipd_input = CipdInput(
1064 client_package=None,
1065 packages=cipd_packages,
1066 server=None)
1067
maruela9fe2cb2017-05-10 10:43:23 -07001068 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001069 secret_bytes = None
1070 if options.secret_bytes_path:
1071 with open(options.secret_bytes_path, 'r') as f:
1072 secret_bytes = f.read().encode('base64')
1073
maruela9fe2cb2017-05-10 10:43:23 -07001074 # Named caches
maruel681d6802017-01-17 16:56:03 -08001075 caches = [
1076 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1077 for i in options.named_cache
1078 ]
maruela9fe2cb2017-05-10 10:43:23 -07001079
maruel77f720b2015-09-15 12:35:22 -07001080 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001081 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001082 cipd_input=cipd_input,
maruela9fe2cb2017-05-10 10:43:23 -07001083 command=command,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001084 dimensions=options.dimensions,
1085 env=options.env,
maruel77f720b2015-09-15 12:35:22 -07001086 execution_timeout_secs=options.hard_timeout,
maruela9fe2cb2017-05-10 10:43:23 -07001087 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001088 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001089 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001090 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001091 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001092 outputs=options.output,
1093 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001094
maruel77f720b2015-09-15 12:35:22 -07001095 return NewTaskRequest(
1096 expiration_secs=options.expiration,
maruela9fe2cb2017-05-10 10:43:23 -07001097 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001098 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001099 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001100 properties=properties,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001101 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001102 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001103 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001104
1105
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001106class TaskOutputStdoutOption(optparse.Option):
1107 """Where to output the each task's console output (stderr/stdout).
1108
1109 The output will be;
1110 none - not be downloaded.
1111 json - stored in summary.json file *only*.
1112 console - shown on stdout *only*.
1113 all - stored in summary.json and shown on stdout.
1114 """
1115
1116 choices = ['all', 'json', 'console', 'none']
1117
1118 def __init__(self, *args, **kw):
1119 optparse.Option.__init__(
1120 self,
1121 *args,
1122 choices=self.choices,
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001123 default=['console', 'json'],
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001124 help=re.sub('\s\s*', ' ', self.__doc__),
1125 **kw)
1126
1127 def convert_value(self, opt, value):
1128 if value not in self.choices:
1129 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1130 self.get_opt_string(), self.choices, value))
1131 stdout_to = []
1132 if value == 'all':
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001133 stdout_to = ['console', 'json']
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001134 elif value != 'none':
1135 stdout_to = [value]
1136 return stdout_to
1137
1138
maruel@chromium.org0437a732013-08-27 16:05:52 +00001139def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001140 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001141 '-t', '--timeout', type='float',
1142 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1143 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001144 parser.group_logging.add_option(
1145 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001146 parser.group_logging.add_option(
1147 '--print-status-updates', action='store_true',
1148 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001149 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001150 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001151 '--task-summary-json',
1152 metavar='FILE',
1153 help='Dump a summary of task results to this file as json. It contains '
1154 'only shards statuses as know to server directly. Any output files '
1155 'emitted by the task can be collected by using --task-output-dir')
1156 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001157 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001158 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001159 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001160 'directory contains per-shard directory with output files produced '
1161 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001162 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001163 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001164 parser.task_output_group.add_option(
1165 '--perf', action='store_true', default=False,
1166 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001167 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001168
1169
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001170@subcommand.usage('bots...')
1171def CMDbot_delete(parser, args):
1172 """Forcibly deletes bots from the Swarming server."""
1173 parser.add_option(
1174 '-f', '--force', action='store_true',
1175 help='Do not prompt for confirmation')
1176 options, args = parser.parse_args(args)
1177 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001178 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001179
1180 bots = sorted(args)
1181 if not options.force:
1182 print('Delete the following bots?')
1183 for bot in bots:
1184 print(' %s' % bot)
1185 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1186 print('Goodbye.')
1187 return 1
1188
1189 result = 0
1190 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001191 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001192 if net.url_read_json(url, data={}, method='POST') is None:
1193 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001194 result = 1
1195 return result
1196
1197
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001198def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001199 """Returns information about the bots connected to the Swarming server."""
1200 add_filter_options(parser)
1201 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001202 '--dead-only', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001203 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001204 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001205 '-k', '--keep-dead', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001206 help='Keep both dead and alive bots')
1207 parser.filter_group.add_option(
1208 '--busy', action='store_true', help='Keep only busy bots')
1209 parser.filter_group.add_option(
1210 '--idle', action='store_true', help='Keep only idle bots')
1211 parser.filter_group.add_option(
1212 '--mp', action='store_true',
1213 help='Keep only Machine Provider managed bots')
1214 parser.filter_group.add_option(
1215 '--non-mp', action='store_true',
1216 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001217 parser.filter_group.add_option(
1218 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001219 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001220 options, args = parser.parse_args(args)
maruel0165e822017-06-08 06:26:53 -07001221 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001222
1223 if options.keep_dead and options.dead_only:
maruel0165e822017-06-08 06:26:53 -07001224 parser.error('Use only one of --keep-dead or --dead-only')
1225 if options.busy and options.idle:
1226 parser.error('Use only one of --busy or --idle')
1227 if options.mp and options.non_mp:
1228 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001229
maruel0165e822017-06-08 06:26:53 -07001230 url = options.swarming + '/api/swarming/v1/bots/list?'
1231 values = []
1232 if options.dead_only:
1233 values.append(('is_dead', 'TRUE'))
1234 elif options.keep_dead:
1235 values.append(('is_dead', 'NONE'))
1236 else:
1237 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001238
maruel0165e822017-06-08 06:26:53 -07001239 if options.busy:
1240 values.append(('is_busy', 'TRUE'))
1241 elif options.idle:
1242 values.append(('is_busy', 'FALSE'))
1243 else:
1244 values.append(('is_busy', 'NONE'))
1245
1246 if options.mp:
1247 values.append(('is_mp', 'TRUE'))
1248 elif options.non_mp:
1249 values.append(('is_mp', 'FALSE'))
1250 else:
1251 values.append(('is_mp', 'NONE'))
1252
1253 for key, value in options.dimensions:
1254 values.append(('dimensions', '%s:%s' % (key, value)))
1255 url += urllib.urlencode(values)
1256 try:
1257 data, yielder = get_yielder(url, 0)
1258 bots = data.get('items') or []
1259 for items in yielder():
1260 if items:
1261 bots.extend(items)
1262 except Failure as e:
1263 sys.stderr.write('\n%s\n' % e)
1264 return 1
maruel77f720b2015-09-15 12:35:22 -07001265 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruel0165e822017-06-08 06:26:53 -07001266 print bot['bot_id']
1267 if not options.bare:
1268 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1269 print ' %s' % json.dumps(dimensions, sort_keys=True)
1270 if bot.get('task_id'):
1271 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001272 return 0
1273
1274
maruelfd0a90c2016-06-10 11:51:10 -07001275@subcommand.usage('task_id')
1276def CMDcancel(parser, args):
1277 """Cancels a task."""
1278 options, args = parser.parse_args(args)
1279 if not args:
1280 parser.error('Please specify the task to cancel')
1281 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001282 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001283 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1284 print('Deleting %s failed. Probably already gone' % task_id)
1285 return 1
1286 return 0
1287
1288
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001289@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001290def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001291 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001292
1293 The result can be in multiple part if the execution was sharded. It can
1294 potentially have retries.
1295 """
1296 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001297 parser.add_option(
1298 '-j', '--json',
1299 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001300 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001301 if not args and not options.json:
1302 parser.error('Must specify at least one task id or --json.')
1303 if args and options.json:
1304 parser.error('Only use one of task id or --json.')
1305
1306 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001307 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001308 try:
maruel1ceb3872015-10-14 06:10:44 -07001309 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001310 data = json.load(f)
1311 except (IOError, ValueError):
1312 parser.error('Failed to open %s' % options.json)
1313 try:
1314 tasks = sorted(
1315 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1316 args = [t['task_id'] for t in tasks]
1317 except (KeyError, TypeError):
1318 parser.error('Failed to process %s' % options.json)
1319 if options.timeout is None:
1320 options.timeout = (
1321 data['request']['properties']['execution_timeout_secs'] +
1322 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001323 else:
1324 valid = frozenset('0123456789abcdef')
1325 if any(not valid.issuperset(task_id) for task_id in args):
1326 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001327
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001328 try:
1329 return collect(
1330 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001331 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001332 options.timeout,
1333 options.decorate,
1334 options.print_status_updates,
1335 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001336 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001337 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001338 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001339 except Failure:
1340 on_error.report(None)
1341 return 1
1342
1343
maruel77f720b2015-09-15 12:35:22 -07001344@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001345def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001346 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1347 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001348
1349 Examples:
maruel0165e822017-06-08 06:26:53 -07001350 Raw task request and results:
1351 swarming.py query -S server-url.com task/123456/request
1352 swarming.py query -S server-url.com task/123456/result
1353
maruel77f720b2015-09-15 12:35:22 -07001354 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001355 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001356
maruel0165e822017-06-08 06:26:53 -07001357 Listing last 10 tasks on a specific bot named 'bot1':
1358 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001359
maruel0165e822017-06-08 06:26:53 -07001360 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001361 quoting is important!:
1362 swarming.py query -S server-url.com --limit 10 \\
maruel0165e822017-06-08 06:26:53 -07001363 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001364 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001365 parser.add_option(
1366 '-L', '--limit', type='int', default=200,
1367 help='Limit to enforce on limitless items (like number of tasks); '
1368 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001369 parser.add_option(
1370 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001371 parser.add_option(
1372 '--progress', action='store_true',
1373 help='Prints a dot at each request to show progress')
1374 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001375 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001376 parser.error(
1377 'Must specify only method name and optionally query args properly '
1378 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001379 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruel0165e822017-06-08 06:26:53 -07001380 try:
1381 data, yielder = get_yielder(base_url, options.limit)
1382 for items in yielder():
1383 if items:
1384 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001385 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001386 sys.stderr.write('.')
1387 sys.stderr.flush()
1388 except Failure as e:
1389 sys.stderr.write('\n%s\n' % e)
1390 return 1
maruel77f720b2015-09-15 12:35:22 -07001391 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001392 sys.stderr.write('\n')
1393 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001394 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001395 options.json = unicode(os.path.abspath(options.json))
1396 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001397 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001398 try:
maruel77f720b2015-09-15 12:35:22 -07001399 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001400 sys.stdout.write('\n')
1401 except IOError:
1402 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001403 return 0
1404
1405
maruel77f720b2015-09-15 12:35:22 -07001406def CMDquery_list(parser, args):
1407 """Returns list of all the Swarming APIs that can be used with command
1408 'query'.
1409 """
1410 parser.add_option(
1411 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1412 options, args = parser.parse_args(args)
1413 if args:
1414 parser.error('No argument allowed.')
1415
1416 try:
1417 apis = endpoints_api_discovery_apis(options.swarming)
1418 except APIError as e:
1419 parser.error(str(e))
1420 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001421 options.json = unicode(os.path.abspath(options.json))
1422 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001423 json.dump(apis, f)
1424 else:
1425 help_url = (
1426 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1427 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001428 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1429 if i:
1430 print('')
maruel77f720b2015-09-15 12:35:22 -07001431 print api_id
maruel11e31af2017-02-15 07:30:50 -08001432 print ' ' + api['description'].strip()
1433 if 'resources' in api:
1434 # Old.
1435 for j, (resource_name, resource) in enumerate(
1436 sorted(api['resources'].iteritems())):
1437 if j:
1438 print('')
1439 for method_name, method in sorted(resource['methods'].iteritems()):
1440 # Only list the GET ones.
1441 if method['httpMethod'] != 'GET':
1442 continue
1443 print '- %s.%s: %s' % (
1444 resource_name, method_name, method['path'])
1445 print('\n'.join(
1446 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1447 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1448 else:
1449 # New.
1450 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001451 # Only list the GET ones.
1452 if method['httpMethod'] != 'GET':
1453 continue
maruel11e31af2017-02-15 07:30:50 -08001454 print '- %s: %s' % (method['id'], method['path'])
1455 print('\n'.join(
1456 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001457 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1458 return 0
1459
1460
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001461@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001462def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001463 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001464
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001465 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001466 """
1467 add_trigger_options(parser)
1468 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001469 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001470 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001471 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001472 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001473 tasks = trigger_task_shards(
1474 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001475 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001476 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001477 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001478 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001479 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001480 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001481 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001482 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001483 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001484 task_ids = [
1485 t['task_id']
1486 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1487 ]
maruel71c61c82016-02-22 06:52:05 -08001488 if options.timeout is None:
1489 options.timeout = (
1490 task_request.properties.execution_timeout_secs +
1491 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001492 try:
1493 return collect(
1494 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001495 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001496 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001497 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001498 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001499 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001500 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001501 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001502 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001503 except Failure:
1504 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001505 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001506
1507
maruel18122c62015-10-23 06:31:23 -07001508@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001509def CMDreproduce(parser, args):
1510 """Runs a task locally that was triggered on the server.
1511
1512 This running locally the same commands that have been run on the bot. The data
1513 downloaded will be in a subdirectory named 'work' of the current working
1514 directory.
maruel18122c62015-10-23 06:31:23 -07001515
1516 You can pass further additional arguments to the target command by passing
1517 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001518 """
maruelc070e672016-02-22 17:32:57 -08001519 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001520 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001521 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001522 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001523 extra_args = []
1524 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001525 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001526 if len(args) > 1:
1527 if args[1] == '--':
1528 if len(args) > 2:
1529 extra_args = args[2:]
1530 else:
1531 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001532
maruel380e3262016-08-31 16:10:06 -07001533 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001534 request = net.url_read_json(url)
1535 if not request:
1536 print >> sys.stderr, 'Failed to retrieve request data for the task'
1537 return 1
1538
maruel12e30012015-10-09 11:55:35 -07001539 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001540 if fs.isdir(workdir):
1541 parser.error('Please delete the directory \'work\' first')
1542 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001543 cachedir = unicode(os.path.abspath('cipd_cache'))
1544 if not fs.exists(cachedir):
1545 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001546
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001547 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001548 env = os.environ.copy()
1549 env['SWARMING_BOT_ID'] = 'reproduce'
1550 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001551 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001552 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001553 for i in properties['env']:
1554 key = i['key'].encode('utf-8')
1555 if not i['value']:
1556 env.pop(key, None)
1557 else:
1558 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001559
iannucci31ab9192017-05-02 19:11:56 -07001560 command = []
nodir152cba62016-05-12 16:08:56 -07001561 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001562 # Create the tree.
1563 with isolateserver.get_storage(
1564 properties['inputs_ref']['isolatedserver'],
1565 properties['inputs_ref']['namespace']) as storage:
1566 bundle = isolateserver.fetch_isolated(
1567 properties['inputs_ref']['isolated'],
1568 storage,
1569 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001570 workdir,
1571 False)
maruel29ab2fd2015-10-16 11:44:01 -07001572 command = bundle.command
1573 if bundle.relative_cwd:
1574 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001575 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001576
1577 if properties.get('command'):
1578 command.extend(properties['command'])
1579
1580 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1581 new_command = tools.fix_python_path(command)
1582 new_command = run_isolated.process_command(
1583 new_command, options.output_dir, None)
1584 if not options.output_dir and new_command != command:
1585 parser.error('The task has outputs, you must use --output-dir')
1586 command = new_command
1587 file_path.ensure_command_has_abs_path(command, workdir)
1588
1589 if properties.get('cipd_input'):
1590 ci = properties['cipd_input']
1591 cp = ci['client_package']
1592 client_manager = cipd.get_client(
1593 ci['server'], cp['package_name'], cp['version'], cachedir)
1594
1595 with client_manager as client:
1596 by_path = collections.defaultdict(list)
1597 for pkg in ci['packages']:
1598 path = pkg['path']
1599 # cipd deals with 'root' as ''
1600 if path == '.':
1601 path = ''
1602 by_path[path].append((pkg['package_name'], pkg['version']))
1603 client.ensure(workdir, by_path, cache_dir=cachedir)
1604
maruel77f720b2015-09-15 12:35:22 -07001605 try:
maruel18122c62015-10-23 06:31:23 -07001606 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001607 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001608 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001609 print >> sys.stderr, str(e)
1610 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001611
1612
maruel0eb1d1b2015-10-02 14:48:21 -07001613@subcommand.usage('bot_id')
1614def CMDterminate(parser, args):
1615 """Tells a bot to gracefully shut itself down as soon as it can.
1616
1617 This is done by completing whatever current task there is then exiting the bot
1618 process.
1619 """
1620 parser.add_option(
1621 '--wait', action='store_true', help='Wait for the bot to terminate')
1622 options, args = parser.parse_args(args)
1623 if len(args) != 1:
1624 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001625 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001626 request = net.url_read_json(url, data={})
1627 if not request:
1628 print >> sys.stderr, 'Failed to ask for termination'
1629 return 1
1630 if options.wait:
1631 return collect(
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001632 options.swarming,
1633 [request['task_id']],
1634 0.,
1635 False,
1636 False,
1637 None,
1638 None,
1639 [],
maruel9531ce02016-04-13 06:11:23 -07001640 False)
maruelb7ded002017-06-10 16:43:17 -07001641 else:
1642 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001643 return 0
1644
1645
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001646@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001647def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001648 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001649
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001650 Passes all extra arguments provided after '--' as additional command line
1651 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001652 """
1653 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001654 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001655 parser.add_option(
1656 '--dump-json',
1657 metavar='FILE',
1658 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001659 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001660 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001661 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001662 tasks = trigger_task_shards(
1663 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001664 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001665 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001666 tasks_sorted = sorted(
1667 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001668 if options.dump_json:
1669 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001670 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001671 'tasks': tasks,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001672 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001673 }
maruel46b015f2015-10-13 18:40:35 -07001674 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001675 print('To collect results, use:')
1676 print(' swarming.py collect -S %s --json %s' %
1677 (options.swarming, options.dump_json))
1678 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001679 print('To collect results, use:')
1680 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001681 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1682 print('Or visit:')
1683 for t in tasks_sorted:
1684 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001685 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001686 except Failure:
1687 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001688 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001689
1690
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001691class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001692 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001693 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001694 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001695 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001696 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001697 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001698 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001699 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001700 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001701 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001702
1703 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001704 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001705 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001706 auth.process_auth_options(self, options)
1707 user = self._process_swarming(options)
1708 if hasattr(options, 'user') and not options.user:
1709 options.user = user
1710 return options, args
1711
1712 def _process_swarming(self, options):
1713 """Processes the --swarming option and aborts if not specified.
1714
1715 Returns the identity as determined by the server.
1716 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001717 if not options.swarming:
1718 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001719 try:
1720 options.swarming = net.fix_url(options.swarming)
1721 except ValueError as e:
1722 self.error('--swarming %s' % e)
1723 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001724 try:
1725 user = auth.ensure_logged_in(options.swarming)
1726 except ValueError as e:
1727 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001728 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001729
1730
1731def main(args):
1732 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001733 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001734
1735
1736if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001737 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001738 fix_encoding.fix_encoding()
1739 tools.disable_buffering()
1740 colorama.init()
1741 sys.exit(main(sys.argv[1:]))