blob: af6a0116f8175462e4fcf283d723b96aa943cf2b [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,
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001109 default=['console', 'json'],
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001110 help=re.sub('\s\s*', ' ', self.__doc__),
1111 **kw)
1112
1113 def convert_value(self, opt, value):
1114 if value not in self.choices:
1115 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1116 self.get_opt_string(), self.choices, value))
1117 stdout_to = []
1118 if value == 'all':
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001119 stdout_to = ['console', 'json']
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001120 elif value != 'none':
1121 stdout_to = [value]
1122 return stdout_to
1123
1124
maruel@chromium.org0437a732013-08-27 16:05:52 +00001125def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001126 parser.server_group.add_option(
maruel71c61c82016-02-22 06:52:05 -08001127 '-t', '--timeout', type='float',
1128 help='Timeout to wait for result, set to 0 for no timeout; default to no '
1129 'wait')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001130 parser.group_logging.add_option(
1131 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001132 parser.group_logging.add_option(
1133 '--print-status-updates', action='store_true',
1134 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001135 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001136 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001137 '--task-summary-json',
1138 metavar='FILE',
1139 help='Dump a summary of task results to this file as json. It contains '
1140 'only shards statuses as know to server directly. Any output files '
1141 'emitted by the task can be collected by using --task-output-dir')
1142 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001143 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001144 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001145 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001146 'directory contains per-shard directory with output files produced '
1147 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001148 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel8294c172017-09-12 18:09:17 -04001149 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001150 parser.task_output_group.add_option(
1151 '--perf', action='store_true', default=False,
1152 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001153 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001154
1155
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001156@subcommand.usage('bots...')
1157def CMDbot_delete(parser, args):
1158 """Forcibly deletes bots from the Swarming server."""
1159 parser.add_option(
1160 '-f', '--force', action='store_true',
1161 help='Do not prompt for confirmation')
1162 options, args = parser.parse_args(args)
1163 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001164 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001165
1166 bots = sorted(args)
1167 if not options.force:
1168 print('Delete the following bots?')
1169 for bot in bots:
1170 print(' %s' % bot)
1171 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1172 print('Goodbye.')
1173 return 1
1174
1175 result = 0
1176 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001177 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001178 if net.url_read_json(url, data={}, method='POST') is None:
1179 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001180 result = 1
1181 return result
1182
1183
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001184def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001185 """Returns information about the bots connected to the Swarming server."""
1186 add_filter_options(parser)
1187 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001188 '--dead-only', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001189 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001190 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001191 '-k', '--keep-dead', action='store_true',
maruel0165e822017-06-08 06:26:53 -07001192 help='Keep both dead and alive bots')
1193 parser.filter_group.add_option(
1194 '--busy', action='store_true', help='Keep only busy bots')
1195 parser.filter_group.add_option(
1196 '--idle', action='store_true', help='Keep only idle bots')
1197 parser.filter_group.add_option(
1198 '--mp', action='store_true',
1199 help='Keep only Machine Provider managed bots')
1200 parser.filter_group.add_option(
1201 '--non-mp', action='store_true',
1202 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001203 parser.filter_group.add_option(
1204 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001205 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001206 options, args = parser.parse_args(args)
maruel0165e822017-06-08 06:26:53 -07001207 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001208
1209 if options.keep_dead and options.dead_only:
maruel0165e822017-06-08 06:26:53 -07001210 parser.error('Use only one of --keep-dead or --dead-only')
1211 if options.busy and options.idle:
1212 parser.error('Use only one of --busy or --idle')
1213 if options.mp and options.non_mp:
1214 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001215
maruel0165e822017-06-08 06:26:53 -07001216 url = options.swarming + '/api/swarming/v1/bots/list?'
1217 values = []
1218 if options.dead_only:
1219 values.append(('is_dead', 'TRUE'))
1220 elif options.keep_dead:
1221 values.append(('is_dead', 'NONE'))
1222 else:
1223 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001224
maruel0165e822017-06-08 06:26:53 -07001225 if options.busy:
1226 values.append(('is_busy', 'TRUE'))
1227 elif options.idle:
1228 values.append(('is_busy', 'FALSE'))
1229 else:
1230 values.append(('is_busy', 'NONE'))
1231
1232 if options.mp:
1233 values.append(('is_mp', 'TRUE'))
1234 elif options.non_mp:
1235 values.append(('is_mp', 'FALSE'))
1236 else:
1237 values.append(('is_mp', 'NONE'))
1238
1239 for key, value in options.dimensions:
1240 values.append(('dimensions', '%s:%s' % (key, value)))
1241 url += urllib.urlencode(values)
1242 try:
1243 data, yielder = get_yielder(url, 0)
1244 bots = data.get('items') or []
1245 for items in yielder():
1246 if items:
1247 bots.extend(items)
1248 except Failure as e:
1249 sys.stderr.write('\n%s\n' % e)
1250 return 1
maruel77f720b2015-09-15 12:35:22 -07001251 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruel0165e822017-06-08 06:26:53 -07001252 print bot['bot_id']
1253 if not options.bare:
1254 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1255 print ' %s' % json.dumps(dimensions, sort_keys=True)
1256 if bot.get('task_id'):
1257 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001258 return 0
1259
1260
maruelfd0a90c2016-06-10 11:51:10 -07001261@subcommand.usage('task_id')
1262def CMDcancel(parser, args):
1263 """Cancels a task."""
1264 options, args = parser.parse_args(args)
1265 if not args:
1266 parser.error('Please specify the task to cancel')
1267 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001268 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
maruelfd0a90c2016-06-10 11:51:10 -07001269 if net.url_read_json(url, data={'task_id': task_id}, method='POST') is None:
1270 print('Deleting %s failed. Probably already gone' % task_id)
1271 return 1
1272 return 0
1273
1274
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001275@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001276def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001277 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001278
1279 The result can be in multiple part if the execution was sharded. It can
1280 potentially have retries.
1281 """
1282 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001283 parser.add_option(
1284 '-j', '--json',
1285 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001286 options, args = parser.parse_args(args)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001287 if not args and not options.json:
1288 parser.error('Must specify at least one task id or --json.')
1289 if args and options.json:
1290 parser.error('Only use one of task id or --json.')
1291
1292 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001293 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001294 try:
maruel1ceb3872015-10-14 06:10:44 -07001295 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001296 data = json.load(f)
1297 except (IOError, ValueError):
1298 parser.error('Failed to open %s' % options.json)
1299 try:
1300 tasks = sorted(
1301 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1302 args = [t['task_id'] for t in tasks]
1303 except (KeyError, TypeError):
1304 parser.error('Failed to process %s' % options.json)
1305 if options.timeout is None:
1306 options.timeout = (
1307 data['request']['properties']['execution_timeout_secs'] +
1308 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001309 else:
1310 valid = frozenset('0123456789abcdef')
1311 if any(not valid.issuperset(task_id) for task_id in args):
1312 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001313
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001314 try:
1315 return collect(
1316 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001317 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001318 options.timeout,
1319 options.decorate,
1320 options.print_status_updates,
1321 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001322 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001323 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001324 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001325 except Failure:
1326 on_error.report(None)
1327 return 1
1328
1329
maruelbea00862015-09-18 09:55:36 -07001330@subcommand.usage('[filename]')
1331def CMDput_bootstrap(parser, args):
1332 """Uploads a new version of bootstrap.py."""
1333 options, args = parser.parse_args(args)
1334 if len(args) != 1:
1335 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001336 url = options.swarming + '/api/swarming/v1/server/put_bootstrap'
maruel1ceb3872015-10-14 06:10:44 -07001337 path = unicode(os.path.abspath(args[0]))
1338 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001339 content = f.read().decode('utf-8')
1340 data = net.url_read_json(url, data={'content': content})
1341 print data
1342 return 0
1343
1344
1345@subcommand.usage('[filename]')
1346def CMDput_bot_config(parser, args):
1347 """Uploads a new version of bot_config.py."""
1348 options, args = parser.parse_args(args)
1349 if len(args) != 1:
1350 parser.error('Must specify file to upload')
maruel380e3262016-08-31 16:10:06 -07001351 url = options.swarming + '/api/swarming/v1/server/put_bot_config'
maruel1ceb3872015-10-14 06:10:44 -07001352 path = unicode(os.path.abspath(args[0]))
1353 with fs.open(path, 'rb') as f:
maruelbea00862015-09-18 09:55:36 -07001354 content = f.read().decode('utf-8')
1355 data = net.url_read_json(url, data={'content': content})
1356 print data
1357 return 0
1358
1359
maruel77f720b2015-09-15 12:35:22 -07001360@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001361def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001362 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1363 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001364
1365 Examples:
maruel0165e822017-06-08 06:26:53 -07001366 Raw task request and results:
1367 swarming.py query -S server-url.com task/123456/request
1368 swarming.py query -S server-url.com task/123456/result
1369
maruel77f720b2015-09-15 12:35:22 -07001370 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001371 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001372
maruel0165e822017-06-08 06:26:53 -07001373 Listing last 10 tasks on a specific bot named 'bot1':
1374 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001375
maruel0165e822017-06-08 06:26:53 -07001376 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001377 quoting is important!:
1378 swarming.py query -S server-url.com --limit 10 \\
maruel0165e822017-06-08 06:26:53 -07001379 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001380 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001381 parser.add_option(
1382 '-L', '--limit', type='int', default=200,
1383 help='Limit to enforce on limitless items (like number of tasks); '
1384 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001385 parser.add_option(
1386 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001387 parser.add_option(
1388 '--progress', action='store_true',
1389 help='Prints a dot at each request to show progress')
1390 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001391 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001392 parser.error(
1393 'Must specify only method name and optionally query args properly '
1394 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001395 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruel0165e822017-06-08 06:26:53 -07001396 try:
1397 data, yielder = get_yielder(base_url, options.limit)
1398 for items in yielder():
1399 if items:
1400 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001401 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001402 sys.stderr.write('.')
1403 sys.stderr.flush()
1404 except Failure as e:
1405 sys.stderr.write('\n%s\n' % e)
1406 return 1
maruel77f720b2015-09-15 12:35:22 -07001407 if options.progress:
maruel0165e822017-06-08 06:26:53 -07001408 sys.stderr.write('\n')
1409 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001410 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001411 options.json = unicode(os.path.abspath(options.json))
1412 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001413 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001414 try:
maruel77f720b2015-09-15 12:35:22 -07001415 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001416 sys.stdout.write('\n')
1417 except IOError:
1418 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001419 return 0
1420
1421
maruel77f720b2015-09-15 12:35:22 -07001422def CMDquery_list(parser, args):
1423 """Returns list of all the Swarming APIs that can be used with command
1424 'query'.
1425 """
1426 parser.add_option(
1427 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1428 options, args = parser.parse_args(args)
1429 if args:
1430 parser.error('No argument allowed.')
1431
1432 try:
1433 apis = endpoints_api_discovery_apis(options.swarming)
1434 except APIError as e:
1435 parser.error(str(e))
1436 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001437 options.json = unicode(os.path.abspath(options.json))
1438 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001439 json.dump(apis, f)
1440 else:
1441 help_url = (
1442 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1443 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001444 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1445 if i:
1446 print('')
maruel77f720b2015-09-15 12:35:22 -07001447 print api_id
maruel11e31af2017-02-15 07:30:50 -08001448 print ' ' + api['description'].strip()
1449 if 'resources' in api:
1450 # Old.
1451 for j, (resource_name, resource) in enumerate(
1452 sorted(api['resources'].iteritems())):
1453 if j:
1454 print('')
1455 for method_name, method in sorted(resource['methods'].iteritems()):
1456 # Only list the GET ones.
1457 if method['httpMethod'] != 'GET':
1458 continue
1459 print '- %s.%s: %s' % (
1460 resource_name, method_name, method['path'])
1461 print('\n'.join(
1462 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1463 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1464 else:
1465 # New.
1466 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001467 # Only list the GET ones.
1468 if method['httpMethod'] != 'GET':
1469 continue
maruel11e31af2017-02-15 07:30:50 -08001470 print '- %s: %s' % (method['id'], method['path'])
1471 print('\n'.join(
1472 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001473 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1474 return 0
1475
1476
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001477@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001478def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001479 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001480
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001481 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001482 """
1483 add_trigger_options(parser)
1484 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001485 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001486 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001487 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001488 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001489 tasks = trigger_task_shards(
1490 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001491 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001492 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001493 'Failed to trigger %s(%s): %s' %
maruela9fe2cb2017-05-10 10:43:23 -07001494 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001495 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001496 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001497 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001498 return 1
maruela9fe2cb2017-05-10 10:43:23 -07001499 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001500 task_ids = [
1501 t['task_id']
1502 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1503 ]
maruel71c61c82016-02-22 06:52:05 -08001504 if options.timeout is None:
1505 options.timeout = (
1506 task_request.properties.execution_timeout_secs +
1507 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001508 try:
1509 return collect(
1510 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001511 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001512 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001513 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001514 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001515 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001516 options.task_output_dir,
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001517 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001518 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001519 except Failure:
1520 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001521 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001522
1523
maruel18122c62015-10-23 06:31:23 -07001524@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001525def CMDreproduce(parser, args):
1526 """Runs a task locally that was triggered on the server.
1527
1528 This running locally the same commands that have been run on the bot. The data
1529 downloaded will be in a subdirectory named 'work' of the current working
1530 directory.
maruel18122c62015-10-23 06:31:23 -07001531
1532 You can pass further additional arguments to the target command by passing
1533 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001534 """
maruelc070e672016-02-22 17:32:57 -08001535 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001536 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001537 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001538 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001539 extra_args = []
1540 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001541 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001542 if len(args) > 1:
1543 if args[1] == '--':
1544 if len(args) > 2:
1545 extra_args = args[2:]
1546 else:
1547 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001548
maruel380e3262016-08-31 16:10:06 -07001549 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001550 request = net.url_read_json(url)
1551 if not request:
1552 print >> sys.stderr, 'Failed to retrieve request data for the task'
1553 return 1
1554
maruel12e30012015-10-09 11:55:35 -07001555 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001556 if fs.isdir(workdir):
1557 parser.error('Please delete the directory \'work\' first')
1558 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001559 cachedir = unicode(os.path.abspath('cipd_cache'))
1560 if not fs.exists(cachedir):
1561 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001562
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001563 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001564 env = os.environ.copy()
1565 env['SWARMING_BOT_ID'] = 'reproduce'
1566 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001567 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001568 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001569 for i in properties['env']:
1570 key = i['key'].encode('utf-8')
1571 if not i['value']:
1572 env.pop(key, None)
1573 else:
1574 env[key] = i['value'].encode('utf-8')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001575
iannucci31ab9192017-05-02 19:11:56 -07001576 command = []
nodir152cba62016-05-12 16:08:56 -07001577 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001578 # Create the tree.
1579 with isolateserver.get_storage(
1580 properties['inputs_ref']['isolatedserver'],
1581 properties['inputs_ref']['namespace']) as storage:
1582 bundle = isolateserver.fetch_isolated(
1583 properties['inputs_ref']['isolated'],
1584 storage,
1585 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001586 workdir,
1587 False)
maruel29ab2fd2015-10-16 11:44:01 -07001588 command = bundle.command
1589 if bundle.relative_cwd:
1590 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001591 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001592
1593 if properties.get('command'):
1594 command.extend(properties['command'])
1595
1596 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
1597 new_command = tools.fix_python_path(command)
1598 new_command = run_isolated.process_command(
1599 new_command, options.output_dir, None)
1600 if not options.output_dir and new_command != command:
1601 parser.error('The task has outputs, you must use --output-dir')
1602 command = new_command
1603 file_path.ensure_command_has_abs_path(command, workdir)
1604
1605 if properties.get('cipd_input'):
1606 ci = properties['cipd_input']
1607 cp = ci['client_package']
1608 client_manager = cipd.get_client(
1609 ci['server'], cp['package_name'], cp['version'], cachedir)
1610
1611 with client_manager as client:
1612 by_path = collections.defaultdict(list)
1613 for pkg in ci['packages']:
1614 path = pkg['path']
1615 # cipd deals with 'root' as ''
1616 if path == '.':
1617 path = ''
1618 by_path[path].append((pkg['package_name'], pkg['version']))
1619 client.ensure(workdir, by_path, cache_dir=cachedir)
1620
maruel77f720b2015-09-15 12:35:22 -07001621 try:
maruel18122c62015-10-23 06:31:23 -07001622 return subprocess.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001623 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001624 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001625 print >> sys.stderr, str(e)
1626 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001627
1628
maruel0eb1d1b2015-10-02 14:48:21 -07001629@subcommand.usage('bot_id')
1630def CMDterminate(parser, args):
1631 """Tells a bot to gracefully shut itself down as soon as it can.
1632
1633 This is done by completing whatever current task there is then exiting the bot
1634 process.
1635 """
1636 parser.add_option(
1637 '--wait', action='store_true', help='Wait for the bot to terminate')
1638 options, args = parser.parse_args(args)
1639 if len(args) != 1:
1640 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001641 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001642 request = net.url_read_json(url, data={})
1643 if not request:
1644 print >> sys.stderr, 'Failed to ask for termination'
1645 return 1
1646 if options.wait:
1647 return collect(
Tim 'mithro' Ansell4c3f1682017-09-08 09:32:52 +10001648 options.swarming,
1649 [request['task_id']],
1650 0.,
1651 False,
1652 False,
1653 None,
1654 None,
1655 [],
maruel9531ce02016-04-13 06:11:23 -07001656 False)
maruelb7ded002017-06-10 16:43:17 -07001657 else:
1658 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001659 return 0
1660
1661
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001662@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001663def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001664 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001665
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001666 Passes all extra arguments provided after '--' as additional command line
1667 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001668 """
1669 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001670 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001671 parser.add_option(
1672 '--dump-json',
1673 metavar='FILE',
1674 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001675 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001676 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001677 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001678 tasks = trigger_task_shards(
1679 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001680 if tasks:
maruela9fe2cb2017-05-10 10:43:23 -07001681 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001682 tasks_sorted = sorted(
1683 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001684 if options.dump_json:
1685 data = {
maruela9fe2cb2017-05-10 10:43:23 -07001686 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001687 'tasks': tasks,
Vadim Shtayura9aef3f12017-08-14 17:41:24 -07001688 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001689 }
maruel46b015f2015-10-13 18:40:35 -07001690 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001691 print('To collect results, use:')
1692 print(' swarming.py collect -S %s --json %s' %
1693 (options.swarming, options.dump_json))
1694 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001695 print('To collect results, use:')
1696 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001697 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1698 print('Or visit:')
1699 for t in tasks_sorted:
1700 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001701 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001702 except Failure:
1703 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001704 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001705
1706
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001707class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001708 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001709 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001710 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001711 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001712 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001713 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001714 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001715 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001716 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001717 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001718
1719 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001720 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001721 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001722 auth.process_auth_options(self, options)
1723 user = self._process_swarming(options)
1724 if hasattr(options, 'user') and not options.user:
1725 options.user = user
1726 return options, args
1727
1728 def _process_swarming(self, options):
1729 """Processes the --swarming option and aborts if not specified.
1730
1731 Returns the identity as determined by the server.
1732 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001733 if not options.swarming:
1734 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001735 try:
1736 options.swarming = net.fix_url(options.swarming)
1737 except ValueError as e:
1738 self.error('--swarming %s' % e)
1739 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001740 try:
1741 user = auth.ensure_logged_in(options.swarming)
1742 except ValueError as e:
1743 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001744 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001745
1746
1747def main(args):
1748 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001749 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001750
1751
1752if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001753 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001754 fix_encoding.fix_encoding()
1755 tools.disable_buffering()
1756 colorama.init()
1757 sys.exit(main(sys.argv[1:]))