blob: e3b8beb27a006788366725179a90280c6de54d78 [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
Robert Iannuccifafa7352018-06-13 17:08:17 +00008__version__ = '0.13'
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' Ansell5e8001d2017-09-08 09:32:52 +100016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import sys
maruel11e31af2017-02-15 07:30:50 -080018import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
iannucci31ab9192017-05-02 19:11:56 -070038import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040040import local_caching
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
maruel0a25f6c2017-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:
maruel0a25f6c2017-05-10 10:43:23 -070056 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070057 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070058 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-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.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080099StringListPair = collections.namedtuple(
100 'StringListPair', [
101 'key',
102 'value', # repeated string
103 ]
104)
105
106
107# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700108TaskProperties = collections.namedtuple(
109 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500110 [
maruel681d6802017-01-17 16:56:03 -0800111 'caches',
borenet02f772b2016-06-22 12:42:19 -0700112 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500113 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500114 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500115 'dimensions',
116 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800117 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700118 'execution_timeout_secs',
119 'extra_args',
120 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500121 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700122 'inputs_ref',
123 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700124 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700125 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700126 ])
127
128
129# See ../appengine/swarming/swarming_rpcs.py.
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400130TaskSlice = collections.namedtuple(
131 'TaskSlice',
132 [
133 'expiration_secs',
134 'properties',
135 'wait_for_capacity',
136 ])
137
138
139# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700140NewTaskRequest = collections.namedtuple(
141 'NewTaskRequest',
142 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500143 'name',
maruel77f720b2015-09-15 12:35:22 -0700144 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500145 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400146 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700147 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500148 'tags',
149 'user',
Robert Iannuccifafa7352018-06-13 17:08:17 +0000150 'pool_task_template',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500151 ])
152
153
maruel77f720b2015-09-15 12:35:22 -0700154def namedtuple_to_dict(value):
155 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400156 if hasattr(value, '_asdict'):
157 return namedtuple_to_dict(value._asdict())
158 if isinstance(value, (list, tuple)):
159 return [namedtuple_to_dict(v) for v in value]
160 if isinstance(value, dict):
161 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
162 return value
maruel77f720b2015-09-15 12:35:22 -0700163
164
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700165def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800166 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700167
168 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500169 """
maruel77f720b2015-09-15 12:35:22 -0700170 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700171 # Don't send 'service_account' if it is None to avoid confusing older
172 # version of the server that doesn't know about 'service_account' and don't
173 # use it at all.
174 if not out['service_account']:
175 out.pop('service_account')
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400176 out['task_slices'][0]['properties']['dimensions'] = [
maruel77f720b2015-09-15 12:35:22 -0700177 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400178 for k, v in out['task_slices'][0]['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700179 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400180 out['task_slices'][0]['properties']['env'] = [
maruel77f720b2015-09-15 12:35:22 -0700181 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400182 for k, v in out['task_slices'][0]['properties']['env'].iteritems()
maruel77f720b2015-09-15 12:35:22 -0700183 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400184 out['task_slices'][0]['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700185 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500186
187
maruel77f720b2015-09-15 12:35:22 -0700188def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500189 """Triggers a request on the Swarming server and returns the json data.
190
191 It's the low-level function.
192
193 Returns:
194 {
195 'request': {
196 'created_ts': u'2010-01-02 03:04:05',
197 'name': ..
198 },
199 'task_id': '12300',
200 }
201 """
202 logging.info('Triggering: %s', raw_request['name'])
203
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500204 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700205 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500206 if not result:
207 on_error.report('Failed to trigger task %s' % raw_request['name'])
208 return None
maruele557bce2015-11-17 09:01:27 -0800209 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800210 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800211 msg = 'Failed to trigger task %s' % raw_request['name']
212 if result['error'].get('errors'):
213 for err in result['error']['errors']:
214 if err.get('message'):
215 msg += '\nMessage: %s' % err['message']
216 if err.get('debugInfo'):
217 msg += '\nDebug info:\n%s' % err['debugInfo']
218 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800219 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800220
221 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800222 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500223 return result
224
225
226def setup_googletest(env, shards, index):
227 """Sets googletest specific environment variables."""
228 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700229 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
230 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
231 env = env[:]
232 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
233 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500234 return env
235
236
237def trigger_task_shards(swarming, task_request, shards):
238 """Triggers one or many subtasks of a sharded task.
239
240 Returns:
241 Dict with task details, returned to caller as part of --dump-json output.
242 None in case of failure.
243 """
244 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700245 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500246 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400247 req['task_slices'][0]['properties']['env'] = setup_googletest(
248 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700249 req['name'] += ':%s:%s' % (index, shards)
250 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251
252 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500253 tasks = {}
254 priority_warning = False
255 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700256 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500257 if not task:
258 break
259 logging.info('Request result: %s', task)
260 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400261 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500262 priority_warning = True
263 print >> sys.stderr, (
264 'Priority was reset to %s' % task['request']['priority'])
265 tasks[request['name']] = {
266 'shard_index': index,
267 'task_id': task['task_id'],
268 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
269 }
270
271 # Some shards weren't triggered. Abort everything.
272 if len(tasks) != len(requests):
273 if tasks:
274 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
275 len(tasks), len(requests))
276 for task_dict in tasks.itervalues():
277 abort_task(swarming, task_dict['task_id'])
278 return None
279
280 return tasks
281
282
283### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000284
285
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700286# How often to print status updates to stdout in 'collect'.
287STATUS_UPDATE_INTERVAL = 15 * 60.
288
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400289
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400290class State(object):
291 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000292
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400293 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
294 values are part of the API so if they change, the API changed.
295
296 It's in fact an enum. Values should be in decreasing order of importance.
297 """
298 RUNNING = 0x10
299 PENDING = 0x20
300 EXPIRED = 0x30
301 TIMED_OUT = 0x40
302 BOT_DIED = 0x50
303 CANCELED = 0x60
304 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400305 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400306 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400307
maruel77f720b2015-09-15 12:35:22 -0700308 STATES = (
309 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400310 'COMPLETED', 'KILLED', 'NO_RESOURCE')
maruel77f720b2015-09-15 12:35:22 -0700311 STATES_RUNNING = ('RUNNING', 'PENDING')
312 STATES_NOT_RUNNING = (
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400313 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED', 'KILLED',
314 'NO_RESOURCE')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400315 STATES_DONE = ('TIMED_OUT', 'COMPLETED', 'KILLED')
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400316 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED', 'NO_RESOURCE')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400317
318 _NAMES = {
319 RUNNING: 'Running',
320 PENDING: 'Pending',
321 EXPIRED: 'Expired',
322 TIMED_OUT: 'Execution timed out',
323 BOT_DIED: 'Bot died',
324 CANCELED: 'User canceled',
325 COMPLETED: 'Completed',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400326 KILLED: 'User killed',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400327 NO_RESOURCE: 'No resource',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400328 }
329
maruel77f720b2015-09-15 12:35:22 -0700330 _ENUMS = {
331 'RUNNING': RUNNING,
332 'PENDING': PENDING,
333 'EXPIRED': EXPIRED,
334 'TIMED_OUT': TIMED_OUT,
335 'BOT_DIED': BOT_DIED,
336 'CANCELED': CANCELED,
337 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400338 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400339 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700340 }
341
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400342 @classmethod
343 def to_string(cls, state):
344 """Returns a user-readable string representing a State."""
345 if state not in cls._NAMES:
346 raise ValueError('Invalid state %s' % state)
347 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000348
maruel77f720b2015-09-15 12:35:22 -0700349 @classmethod
350 def from_enum(cls, state):
351 """Returns int value based on the string."""
352 if state not in cls._ENUMS:
353 raise ValueError('Invalid state %s' % state)
354 return cls._ENUMS[state]
355
maruel@chromium.org0437a732013-08-27 16:05:52 +0000356
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700357class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700358 """Assembles task execution summary (for --task-summary-json output).
359
360 Optionally fetches task outputs from isolate server to local disk (used when
361 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362
363 This object is shared among multiple threads running 'retrieve_results'
364 function, in particular they call 'process_shard_result' method in parallel.
365 """
366
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000367 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700368 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
369
370 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700371 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 shard_count: expected number of task shards.
373 """
maruel12e30012015-10-09 11:55:35 -0700374 self.task_output_dir = (
375 unicode(os.path.abspath(task_output_dir))
376 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000377 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700378 self.shard_count = shard_count
379
380 self._lock = threading.Lock()
381 self._per_shard_results = {}
382 self._storage = None
383
nodire5028a92016-04-29 14:38:21 -0700384 if self.task_output_dir:
385 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386
Vadim Shtayurab450c602014-05-12 19:23:25 -0700387 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700388 """Stores results of a single task shard, fetches output files if necessary.
389
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400390 Modifies |result| in place.
391
maruel77f720b2015-09-15 12:35:22 -0700392 shard_index is 0-based.
393
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700394 Called concurrently from multiple threads.
395 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700396 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700397 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700398 if shard_index < 0 or shard_index >= self.shard_count:
399 logging.warning(
400 'Shard index %d is outside of expected range: [0; %d]',
401 shard_index, self.shard_count - 1)
402 return
403
maruel77f720b2015-09-15 12:35:22 -0700404 if result.get('outputs_ref'):
405 ref = result['outputs_ref']
406 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
407 ref['isolatedserver'],
408 urllib.urlencode(
409 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400410
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700411 # Store result dict of that shard, ignore results we've already seen.
412 with self._lock:
413 if shard_index in self._per_shard_results:
414 logging.warning('Ignoring duplicate shard index %d', shard_index)
415 return
416 self._per_shard_results[shard_index] = result
417
418 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700419 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400420 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700421 result['outputs_ref']['isolatedserver'],
422 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400423 if storage:
424 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400425 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
426 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400427 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700428 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400429 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400430 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700431 os.path.join(self.task_output_dir, str(shard_index)),
432 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700433
434 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700435 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700436 with self._lock:
437 # Write an array of shard results with None for missing shards.
438 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700439 'shards': [
440 self._per_shard_results.get(i) for i in xrange(self.shard_count)
441 ],
442 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000443
444 # Don't store stdout in the summary if not requested too.
445 if "json" not in self.task_output_stdout:
446 for shard_json in summary['shards']:
447 if not shard_json:
448 continue
449 if "output" in shard_json:
450 del shard_json["output"]
451 if "outputs" in shard_json:
452 del shard_json["outputs"]
453
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700454 # Write summary.json to task_output_dir as well.
455 if self.task_output_dir:
456 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700457 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700458 summary,
459 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700460 if self._storage:
461 self._storage.close()
462 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700463 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700464
465 def _get_storage(self, isolate_server, namespace):
466 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700467 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700468 with self._lock:
469 if not self._storage:
470 self._storage = isolateserver.get_storage(isolate_server, namespace)
471 else:
472 # Shards must all use exact same isolate server and namespace.
473 if self._storage.location != isolate_server:
474 logging.error(
475 'Task shards are using multiple isolate servers: %s and %s',
476 self._storage.location, isolate_server)
477 return None
478 if self._storage.namespace != namespace:
479 logging.error(
480 'Task shards are using multiple namespaces: %s and %s',
481 self._storage.namespace, namespace)
482 return None
483 return self._storage
484
485
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500486def now():
487 """Exists so it can be mocked easily."""
488 return time.time()
489
490
maruel77f720b2015-09-15 12:35:22 -0700491def parse_time(value):
492 """Converts serialized time from the API to datetime.datetime."""
493 # When microseconds are 0, the '.123456' suffix is elided. This means the
494 # serialized format is not consistent, which confuses the hell out of python.
495 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
496 try:
497 return datetime.datetime.strptime(value, fmt)
498 except ValueError:
499 pass
500 raise ValueError('Failed to parse %s' % value)
501
502
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700503def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700504 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000505 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400506 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700507
Vadim Shtayurab450c602014-05-12 19:23:25 -0700508 Returns:
509 <result dict> on success.
510 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700511 """
maruel71c61c82016-02-22 06:52:05 -0800512 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700513 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700514 if include_perf:
515 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700516 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700517 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400518 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700519 attempt = 0
520
521 while not should_stop.is_set():
522 attempt += 1
523
524 # Waiting for too long -> give up.
525 current_time = now()
526 if deadline and current_time >= deadline:
527 logging.error('retrieve_results(%s) timed out on attempt %d',
528 base_url, attempt)
529 return None
530
531 # Do not spin too fast. Spin faster at the beginning though.
532 # Start with 1 sec delay and for each 30 sec of waiting add another second
533 # of delay, until hitting 15 sec ceiling.
534 if attempt > 1:
535 max_delay = min(15, 1 + (current_time - started) / 30.0)
536 delay = min(max_delay, deadline - current_time) if deadline else max_delay
537 if delay > 0:
538 logging.debug('Waiting %.1f sec before retrying', delay)
539 should_stop.wait(delay)
540 if should_stop.is_set():
541 return None
542
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400543 # Disable internal retries in net.url_read_json, since we are doing retries
544 # ourselves.
545 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700546 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
547 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400548 # Retry on 500s only if no timeout is specified.
549 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400550 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400551 if timeout == -1:
552 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400553 continue
maruel77f720b2015-09-15 12:35:22 -0700554
maruelbf53e042015-12-01 15:00:51 -0800555 if result.get('error'):
556 # An error occurred.
557 if result['error'].get('errors'):
558 for err in result['error']['errors']:
559 logging.warning(
560 'Error while reading task: %s; %s',
561 err.get('message'), err.get('debugInfo'))
562 elif result['error'].get('message'):
563 logging.warning(
564 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400565 if timeout == -1:
566 return result
maruelbf53e042015-12-01 15:00:51 -0800567 continue
568
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400569 # When timeout == -1, always return on first attempt. 500s are already
570 # retried in this case.
571 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000572 if fetch_stdout:
573 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700574 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700575 # Record the result, try to fetch attached output files (if any).
576 if output_collector:
577 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700578 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700579 if result.get('internal_failure'):
580 logging.error('Internal error!')
581 elif result['state'] == 'BOT_DIED':
582 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700583 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000584
585
maruel77f720b2015-09-15 12:35:22 -0700586def convert_to_old_format(result):
587 """Converts the task result data from Endpoints API format to old API format
588 for compatibility.
589
590 This goes into the file generated as --task-summary-json.
591 """
592 # Sets default.
593 result.setdefault('abandoned_ts', None)
594 result.setdefault('bot_id', None)
595 result.setdefault('bot_version', None)
596 result.setdefault('children_task_ids', [])
597 result.setdefault('completed_ts', None)
598 result.setdefault('cost_saved_usd', None)
599 result.setdefault('costs_usd', None)
600 result.setdefault('deduped_from', None)
601 result.setdefault('name', None)
602 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700603 result.setdefault('server_versions', None)
604 result.setdefault('started_ts', None)
605 result.setdefault('tags', None)
606 result.setdefault('user', None)
607
608 # Convertion back to old API.
609 duration = result.pop('duration', None)
610 result['durations'] = [duration] if duration else []
611 exit_code = result.pop('exit_code', None)
612 result['exit_codes'] = [int(exit_code)] if exit_code else []
613 result['id'] = result.pop('task_id')
614 result['isolated_out'] = result.get('outputs_ref', None)
615 output = result.pop('output', None)
616 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700617 # server_version
618 # Endpoints result 'state' as string. For compatibility with old code, convert
619 # to int.
620 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700621 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700622 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700623 if 'bot_dimensions' in result:
624 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700625 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700626 }
627 else:
628 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700629
630
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700631def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400632 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000633 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500634 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000635
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700636 Duplicate shards are ignored. Shards are yielded in order of completion.
637 Timed out shards are NOT yielded at all. Caller can compare number of yielded
638 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639
640 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500641 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 +0000642 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500643
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700644 output_collector is an optional instance of TaskOutputCollector that will be
645 used to fetch files produced by a task from isolate server to the local disk.
646
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500647 Yields:
648 (index, result). In particular, 'result' is defined as the
649 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000651 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400652 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700653 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700654 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700655
maruel@chromium.org0437a732013-08-27 16:05:52 +0000656 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
657 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700658 # Adds a task to the thread pool to call 'retrieve_results' and return
659 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400660 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700661 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000662 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400663 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000664 task_id, timeout, should_stop, output_collector, include_perf,
665 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700666
667 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400668 for shard_index, task_id in enumerate(task_ids):
669 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700670
671 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400672 shards_remaining = range(len(task_ids))
673 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700675 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700676 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700677 shard_index, result = results_channel.pull(
678 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700679 except threading_utils.TaskChannel.Timeout:
680 if print_status_updates:
681 print(
682 'Waiting for results from the following shards: %s' %
683 ', '.join(map(str, shards_remaining)))
684 sys.stdout.flush()
685 continue
686 except Exception:
687 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700688
689 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700690 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000691 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500692 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000693 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700694
Vadim Shtayurab450c602014-05-12 19:23:25 -0700695 # Yield back results to the caller.
696 assert shard_index in shards_remaining
697 shards_remaining.remove(shard_index)
698 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700699
maruel@chromium.org0437a732013-08-27 16:05:52 +0000700 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700701 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000702 should_stop.set()
703
704
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000705def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000706 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700707 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400708 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700709 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
710 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400711 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
712 metadata.get('abandoned_ts')):
713 pending = '%.1fs' % (
714 parse_time(metadata['abandoned_ts']) -
715 parse_time(metadata['created_ts'])
716 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400717 else:
718 pending = 'N/A'
719
maruel77f720b2015-09-15 12:35:22 -0700720 if metadata.get('duration') is not None:
721 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400722 else:
723 duration = 'N/A'
724
maruel77f720b2015-09-15 12:35:22 -0700725 if metadata.get('exit_code') is not None:
726 # Integers are encoded as string to not loose precision.
727 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400728 else:
729 exit_code = 'N/A'
730
731 bot_id = metadata.get('bot_id') or 'N/A'
732
maruel77f720b2015-09-15 12:35:22 -0700733 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400734 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000735 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400736 if metadata.get('state') == 'CANCELED':
737 tag_footer2 = ' Pending: %s CANCELED' % pending
738 elif metadata.get('state') == 'EXPIRED':
739 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400740 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400741 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
742 pending, duration, bot_id, exit_code, metadata['state'])
743 else:
744 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
745 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400746
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000747 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
748 dash_pad = '+-%s-+' % ('-' * tag_len)
749 tag_header = '| %s |' % tag_header.ljust(tag_len)
750 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
751 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400752
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000753 if include_stdout:
754 return '\n'.join([
755 dash_pad,
756 tag_header,
757 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400758 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000759 dash_pad,
760 tag_footer1,
761 tag_footer2,
762 dash_pad,
763 ])
764 else:
765 return '\n'.join([
766 dash_pad,
767 tag_header,
768 tag_footer2,
769 dash_pad,
770 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000771
772
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700774 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000775 task_summary_json, task_output_dir, task_output_stdout,
776 include_perf):
maruela5490782015-09-30 10:56:59 -0700777 """Retrieves results of a Swarming task.
778
779 Returns:
780 process exit code that should be returned to the user.
781 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700782 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000783 output_collector = TaskOutputCollector(
784 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700785
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700786 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700787 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400788 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700789 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400790 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400791 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000792 output_collector, include_perf,
793 (len(task_output_stdout) > 0),
794 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700795 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700796
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400797 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700798 shard_exit_code = metadata.get('exit_code')
799 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700800 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700801 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700802 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400803 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700804 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700805
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700806 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000807 s = decorate_shard_output(
808 swarming, index, metadata,
809 "console" in task_output_stdout).encode(
810 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700811 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400812 if len(seen_shards) < len(task_ids):
813 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700814 else:
maruel77f720b2015-09-15 12:35:22 -0700815 print('%s: %s %s' % (
816 metadata.get('bot_id', 'N/A'),
817 metadata['task_id'],
818 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000819 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700820 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400821 if output:
822 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700823 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700824 summary = output_collector.finalize()
825 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700826 # TODO(maruel): Make this optional.
827 for i in summary['shards']:
828 if i:
829 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700830 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700831
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400832 if decorate and total_duration:
833 print('Total duration: %.1fs' % total_duration)
834
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400835 if len(seen_shards) != len(task_ids):
836 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700837 print >> sys.stderr, ('Results from some shards are missing: %s' %
838 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700839 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700840
maruela5490782015-09-30 10:56:59 -0700841 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000842
843
maruel77f720b2015-09-15 12:35:22 -0700844### API management.
845
846
847class APIError(Exception):
848 pass
849
850
851def endpoints_api_discovery_apis(host):
852 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
853 the APIs exposed by a host.
854
855 https://developers.google.com/discovery/v1/reference/apis/list
856 """
maruel380e3262016-08-31 16:10:06 -0700857 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
858 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700859 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
860 if data is None:
861 raise APIError('Failed to discover APIs on %s' % host)
862 out = {}
863 for api in data['items']:
864 if api['id'] == 'discovery:v1':
865 continue
866 # URL is of the following form:
867 # url = host + (
868 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
869 api_data = net.url_read_json(api['discoveryRestUrl'])
870 if api_data is None:
871 raise APIError('Failed to discover %s on %s' % (api['id'], host))
872 out[api['id']] = api_data
873 return out
874
875
maruelaf6b06c2017-06-08 06:26:53 -0700876def get_yielder(base_url, limit):
877 """Returns the first query and a function that yields following items."""
878 CHUNK_SIZE = 250
879
880 url = base_url
881 if limit:
882 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
883 data = net.url_read_json(url)
884 if data is None:
885 # TODO(maruel): Do basic diagnostic.
886 raise Failure('Failed to access %s' % url)
887 org_cursor = data.pop('cursor', None)
888 org_total = len(data.get('items') or [])
889 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
890 if not org_cursor or not org_total:
891 # This is not an iterable resource.
892 return data, lambda: []
893
894 def yielder():
895 cursor = org_cursor
896 total = org_total
897 # Some items support cursors. Try to get automatically if cursors are needed
898 # by looking at the 'cursor' items.
899 while cursor and (not limit or total < limit):
900 merge_char = '&' if '?' in base_url else '?'
901 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
902 if limit:
903 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
904 new = net.url_read_json(url)
905 if new is None:
906 raise Failure('Failed to access %s' % url)
907 cursor = new.get('cursor')
908 new_items = new.get('items')
909 nb_items = len(new_items or [])
910 total += nb_items
911 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
912 yield new_items
913
914 return data, yielder
915
916
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500917### Commands.
918
919
920def abort_task(_swarming, _manifest):
921 """Given a task manifest that was triggered, aborts its execution."""
922 # TODO(vadimsh): No supported by the server yet.
923
924
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400925def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800926 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500927 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500928 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500929 dest='dimensions', metavar='FOO bar',
930 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500931 parser.add_option_group(parser.filter_group)
932
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400933
maruelaf6b06c2017-06-08 06:26:53 -0700934def process_filter_options(parser, options):
935 for key, value in options.dimensions:
936 if ':' in key:
937 parser.error('--dimension key cannot contain ":"')
938 if key.strip() != key:
939 parser.error('--dimension key has whitespace')
940 if not key:
941 parser.error('--dimension key is empty')
942
943 if value.strip() != value:
944 parser.error('--dimension value has whitespace')
945 if not value:
946 parser.error('--dimension value is empty')
947 options.dimensions.sort()
948
949
Vadim Shtayurab450c602014-05-12 19:23:25 -0700950def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400951 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700952 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700953 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700954 help='Number of shards to trigger and collect.')
955 parser.add_option_group(parser.sharding_group)
956
957
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400958def add_trigger_options(parser):
959 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500960 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400961 add_filter_options(parser)
962
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400963 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800964 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700965 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500966 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800967 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500968 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700969 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800970 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800971 '--env-prefix', default=[], action='append', nargs=2,
972 metavar='VAR local/path',
973 help='Prepend task-relative `local/path` to the task\'s VAR environment '
974 'variable using os-appropriate pathsep character. Can be specified '
975 'multiple times for the same VAR to add multiple paths.')
976 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400977 '--idempotent', action='store_true', default=False,
978 help='When set, the server will actively try to find a previous task '
979 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800980 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700981 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700982 help='The optional path to a file containing the secret_bytes to use with'
983 'this task.')
maruel681d6802017-01-17 16:56:03 -0800984 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700985 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400986 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800987 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700988 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400989 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800990 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500991 '--raw-cmd', action='store_true', default=False,
992 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700993 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800994 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500995 '--relative-cwd',
996 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
997 'requires --raw-cmd')
998 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700999 '--cipd-package', action='append', default=[], metavar='PKG',
1000 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -07001001 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -08001002 group.add_option(
1003 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -07001004 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -08001005 help='"<name> <relpath>" items to keep a persistent bot managed cache')
1006 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -07001007 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001008 help='Email of a service account to run the task as, or literal "bot" '
1009 'string to indicate that the task should use the same account the '
1010 'bot itself is using to authenticate to Swarming. Don\'t use task '
1011 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001012 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +00001013 '--pool-task-template',
1014 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
1015 default='AUTO',
1016 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
1017 'By default, the pool\'s TaskTemplate is automatically selected, '
1018 'according the pool configuration on the server. Choices are: '
1019 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
1020 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001021 '-o', '--output', action='append', default=[], metavar='PATH',
1022 help='A list of files to return in addition to those written to '
1023 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1024 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001025 group.add_option(
1026 '--wait-for-capacity', action='store_true', default=False,
1027 help='Instructs to leave the task PENDING even if there\'s no known bot '
1028 'that could run this task, otherwise the task will be denied with '
1029 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -08001030 parser.add_option_group(group)
1031
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001032 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -08001033 group.add_option(
1034 '--priority', type='int', default=100,
1035 help='The lower value, the more important the task is')
1036 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001037 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001038 help='Display name of the task. Defaults to '
1039 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1040 'isolated file is provided, if a hash is provided, it defaults to '
1041 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1042 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001043 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001044 help='Tags to assign to the task.')
1045 group.add_option(
1046 '--user', default='',
1047 help='User associated with the task. Defaults to authenticated user on '
1048 'the server.')
1049 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001050 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001051 help='Seconds to allow the task to be pending for a bot to run before '
1052 'this task request expires.')
1053 group.add_option(
1054 '--deadline', type='int', dest='expiration',
1055 help=optparse.SUPPRESS_HELP)
1056 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001057
1058
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001059def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001060 """Processes trigger options and does preparatory steps.
1061
1062 Returns:
1063 NewTaskRequest instance.
1064 """
maruelaf6b06c2017-06-08 06:26:53 -07001065 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001066 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001067 if args and args[0] == '--':
1068 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001069
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001070 if not options.dimensions:
1071 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001072 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1073 parser.error('--tags must be in the format key:value')
1074 if options.raw_cmd and not args:
1075 parser.error(
1076 'Arguments with --raw-cmd should be passed after -- as command '
1077 'delimiter.')
1078 if options.isolate_server and not options.namespace:
1079 parser.error(
1080 '--namespace must be a valid value when --isolate-server is used')
1081 if not options.isolated and not options.raw_cmd:
1082 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1083
1084 # Isolated
1085 # --isolated is required only if --raw-cmd wasn't provided.
1086 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1087 # preferred server.
1088 isolateserver.process_isolate_server_options(
1089 parser, options, False, not options.raw_cmd)
1090 inputs_ref = None
1091 if options.isolate_server:
1092 inputs_ref = FilesRef(
1093 isolated=options.isolated,
1094 isolatedserver=options.isolate_server,
1095 namespace=options.namespace)
1096
1097 # Command
1098 command = None
1099 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001100 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001101 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001102 if options.relative_cwd:
1103 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1104 if not a.startswith(os.getcwd()):
1105 parser.error(
1106 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001107 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001108 if options.relative_cwd:
1109 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001110 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001111
maruel0a25f6c2017-05-10 10:43:23 -07001112 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001113 cipd_packages = []
1114 for p in options.cipd_package:
1115 split = p.split(':', 2)
1116 if len(split) != 3:
1117 parser.error('CIPD packages must take the form: path:package:version')
1118 cipd_packages.append(CipdPackage(
1119 package_name=split[1],
1120 path=split[0],
1121 version=split[2]))
1122 cipd_input = None
1123 if cipd_packages:
1124 cipd_input = CipdInput(
1125 client_package=None,
1126 packages=cipd_packages,
1127 server=None)
1128
maruel0a25f6c2017-05-10 10:43:23 -07001129 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001130 secret_bytes = None
1131 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001132 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001133 secret_bytes = f.read().encode('base64')
1134
maruel0a25f6c2017-05-10 10:43:23 -07001135 # Named caches
maruel681d6802017-01-17 16:56:03 -08001136 caches = [
1137 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1138 for i in options.named_cache
1139 ]
maruel0a25f6c2017-05-10 10:43:23 -07001140
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001141 env_prefixes = {}
1142 for k, v in options.env_prefix:
1143 env_prefixes.setdefault(k, []).append(v)
1144
maruel77f720b2015-09-15 12:35:22 -07001145 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001146 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001147 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001148 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001149 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001150 dimensions=options.dimensions,
1151 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001152 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001153 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001154 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001155 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001156 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001157 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001158 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001159 outputs=options.output,
1160 secret_bytes=secret_bytes)
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001161 task_slice = TaskSlice(
1162 expiration_secs=options.expiration,
1163 properties=properties,
1164 wait_for_capacity=options.wait_for_capacity)
maruel77f720b2015-09-15 12:35:22 -07001165 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001166 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001167 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001168 priority=options.priority,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001169 task_slices=[task_slice],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001170 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001171 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001172 user=options.user,
1173 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001174
1175
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001176class TaskOutputStdoutOption(optparse.Option):
1177 """Where to output the each task's console output (stderr/stdout).
1178
1179 The output will be;
1180 none - not be downloaded.
1181 json - stored in summary.json file *only*.
1182 console - shown on stdout *only*.
1183 all - stored in summary.json and shown on stdout.
1184 """
1185
1186 choices = ['all', 'json', 'console', 'none']
1187
1188 def __init__(self, *args, **kw):
1189 optparse.Option.__init__(
1190 self,
1191 *args,
1192 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001193 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001194 help=re.sub('\s\s*', ' ', self.__doc__),
1195 **kw)
1196
1197 def convert_value(self, opt, value):
1198 if value not in self.choices:
1199 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1200 self.get_opt_string(), self.choices, value))
1201 stdout_to = []
1202 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001203 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001204 elif value != 'none':
1205 stdout_to = [value]
1206 return stdout_to
1207
1208
maruel@chromium.org0437a732013-08-27 16:05:52 +00001209def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001210 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001211 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001212 help='Timeout to wait for result, set to -1 for no timeout and get '
1213 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001214 parser.group_logging.add_option(
1215 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001216 parser.group_logging.add_option(
1217 '--print-status-updates', action='store_true',
1218 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001219 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001220 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001221 '--task-summary-json',
1222 metavar='FILE',
1223 help='Dump a summary of task results to this file as json. It contains '
1224 'only shards statuses as know to server directly. Any output files '
1225 'emitted by the task can be collected by using --task-output-dir')
1226 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001227 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001228 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001229 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001230 'directory contains per-shard directory with output files produced '
1231 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001232 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001233 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001234 parser.task_output_group.add_option(
1235 '--perf', action='store_true', default=False,
1236 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001237 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001238
1239
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001240def process_collect_options(parser, options):
1241 # Only negative -1 is allowed, disallow other negative values.
1242 if options.timeout != -1 and options.timeout < 0:
1243 parser.error('Invalid --timeout value')
1244
1245
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001246@subcommand.usage('bots...')
1247def CMDbot_delete(parser, args):
1248 """Forcibly deletes bots from the Swarming server."""
1249 parser.add_option(
1250 '-f', '--force', action='store_true',
1251 help='Do not prompt for confirmation')
1252 options, args = parser.parse_args(args)
1253 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001254 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001255
1256 bots = sorted(args)
1257 if not options.force:
1258 print('Delete the following bots?')
1259 for bot in bots:
1260 print(' %s' % bot)
1261 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1262 print('Goodbye.')
1263 return 1
1264
1265 result = 0
1266 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001267 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001268 if net.url_read_json(url, data={}, method='POST') is None:
1269 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001270 result = 1
1271 return result
1272
1273
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001274def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001275 """Returns information about the bots connected to the Swarming server."""
1276 add_filter_options(parser)
1277 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001278 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001279 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001280 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001281 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001282 help='Keep both dead and alive bots')
1283 parser.filter_group.add_option(
1284 '--busy', action='store_true', help='Keep only busy bots')
1285 parser.filter_group.add_option(
1286 '--idle', action='store_true', help='Keep only idle bots')
1287 parser.filter_group.add_option(
1288 '--mp', action='store_true',
1289 help='Keep only Machine Provider managed bots')
1290 parser.filter_group.add_option(
1291 '--non-mp', action='store_true',
1292 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001293 parser.filter_group.add_option(
1294 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001295 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001296 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001297 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001298
1299 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001300 parser.error('Use only one of --keep-dead or --dead-only')
1301 if options.busy and options.idle:
1302 parser.error('Use only one of --busy or --idle')
1303 if options.mp and options.non_mp:
1304 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001305
smut281c3902018-05-30 17:50:05 -07001306 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001307 values = []
1308 if options.dead_only:
1309 values.append(('is_dead', 'TRUE'))
1310 elif options.keep_dead:
1311 values.append(('is_dead', 'NONE'))
1312 else:
1313 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001314
maruelaf6b06c2017-06-08 06:26:53 -07001315 if options.busy:
1316 values.append(('is_busy', 'TRUE'))
1317 elif options.idle:
1318 values.append(('is_busy', 'FALSE'))
1319 else:
1320 values.append(('is_busy', 'NONE'))
1321
1322 if options.mp:
1323 values.append(('is_mp', 'TRUE'))
1324 elif options.non_mp:
1325 values.append(('is_mp', 'FALSE'))
1326 else:
1327 values.append(('is_mp', 'NONE'))
1328
1329 for key, value in options.dimensions:
1330 values.append(('dimensions', '%s:%s' % (key, value)))
1331 url += urllib.urlencode(values)
1332 try:
1333 data, yielder = get_yielder(url, 0)
1334 bots = data.get('items') or []
1335 for items in yielder():
1336 if items:
1337 bots.extend(items)
1338 except Failure as e:
1339 sys.stderr.write('\n%s\n' % e)
1340 return 1
maruel77f720b2015-09-15 12:35:22 -07001341 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001342 print bot['bot_id']
1343 if not options.bare:
1344 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1345 print ' %s' % json.dumps(dimensions, sort_keys=True)
1346 if bot.get('task_id'):
1347 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001348 return 0
1349
1350
maruelfd0a90c2016-06-10 11:51:10 -07001351@subcommand.usage('task_id')
1352def CMDcancel(parser, args):
1353 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001354 parser.add_option(
1355 '-k', '--kill-running', action='store_true', default=False,
1356 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001357 options, args = parser.parse_args(args)
1358 if not args:
1359 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001360 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001361 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001362 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001363 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001364 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001365 print('Deleting %s failed. Probably already gone' % task_id)
1366 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001367 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001368 return 0
1369
1370
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001371@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001372def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001373 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001374
1375 The result can be in multiple part if the execution was sharded. It can
1376 potentially have retries.
1377 """
1378 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001379 parser.add_option(
1380 '-j', '--json',
1381 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001382 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001383 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001384 if not args and not options.json:
1385 parser.error('Must specify at least one task id or --json.')
1386 if args and options.json:
1387 parser.error('Only use one of task id or --json.')
1388
1389 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001390 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001391 try:
maruel1ceb3872015-10-14 06:10:44 -07001392 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001393 data = json.load(f)
1394 except (IOError, ValueError):
1395 parser.error('Failed to open %s' % options.json)
1396 try:
1397 tasks = sorted(
1398 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1399 args = [t['task_id'] for t in tasks]
1400 except (KeyError, TypeError):
1401 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001402 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001403 # Take in account all the task slices.
1404 offset = 0
1405 for s in data['request']['task_slices']:
1406 m = (offset + s['properties']['execution_timeout_secs'] +
1407 s['expiration_secs'])
1408 if m > options.timeout:
1409 options.timeout = m
1410 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001411 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001412 else:
1413 valid = frozenset('0123456789abcdef')
1414 if any(not valid.issuperset(task_id) for task_id in args):
1415 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001416
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001417 try:
1418 return collect(
1419 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001420 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001421 options.timeout,
1422 options.decorate,
1423 options.print_status_updates,
1424 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001425 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001426 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001427 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001428 except Failure:
1429 on_error.report(None)
1430 return 1
1431
1432
maruel77f720b2015-09-15 12:35:22 -07001433@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001434def CMDpost(parser, args):
1435 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1436
1437 Input data must be sent to stdin, result is printed to stdout.
1438
1439 If HTTP response code >= 400, returns non-zero.
1440 """
1441 options, args = parser.parse_args(args)
1442 if len(args) != 1:
1443 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001444 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001445 data = sys.stdin.read()
1446 try:
1447 resp = net.url_read(url, data=data, method='POST')
1448 except net.TimeoutError:
1449 sys.stderr.write('Timeout!\n')
1450 return 1
1451 if not resp:
1452 sys.stderr.write('No response!\n')
1453 return 1
1454 sys.stdout.write(resp)
1455 return 0
1456
1457
1458@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001459def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001460 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1461 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001462
1463 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001464 Raw task request and results:
1465 swarming.py query -S server-url.com task/123456/request
1466 swarming.py query -S server-url.com task/123456/result
1467
maruel77f720b2015-09-15 12:35:22 -07001468 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001469 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001470
maruelaf6b06c2017-06-08 06:26:53 -07001471 Listing last 10 tasks on a specific bot named 'bot1':
1472 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001473
maruelaf6b06c2017-06-08 06:26:53 -07001474 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001475 quoting is important!:
1476 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001477 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001478 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001479 parser.add_option(
1480 '-L', '--limit', type='int', default=200,
1481 help='Limit to enforce on limitless items (like number of tasks); '
1482 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001483 parser.add_option(
1484 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001485 parser.add_option(
1486 '--progress', action='store_true',
1487 help='Prints a dot at each request to show progress')
1488 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001489 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001490 parser.error(
1491 'Must specify only method name and optionally query args properly '
1492 'escaped.')
smut281c3902018-05-30 17:50:05 -07001493 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001494 try:
1495 data, yielder = get_yielder(base_url, options.limit)
1496 for items in yielder():
1497 if items:
1498 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001499 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001500 sys.stderr.write('.')
1501 sys.stderr.flush()
1502 except Failure as e:
1503 sys.stderr.write('\n%s\n' % e)
1504 return 1
maruel77f720b2015-09-15 12:35:22 -07001505 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001506 sys.stderr.write('\n')
1507 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001508 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001509 options.json = unicode(os.path.abspath(options.json))
1510 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001511 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001512 try:
maruel77f720b2015-09-15 12:35:22 -07001513 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001514 sys.stdout.write('\n')
1515 except IOError:
1516 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001517 return 0
1518
1519
maruel77f720b2015-09-15 12:35:22 -07001520def CMDquery_list(parser, args):
1521 """Returns list of all the Swarming APIs that can be used with command
1522 'query'.
1523 """
1524 parser.add_option(
1525 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1526 options, args = parser.parse_args(args)
1527 if args:
1528 parser.error('No argument allowed.')
1529
1530 try:
1531 apis = endpoints_api_discovery_apis(options.swarming)
1532 except APIError as e:
1533 parser.error(str(e))
1534 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001535 options.json = unicode(os.path.abspath(options.json))
1536 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001537 json.dump(apis, f)
1538 else:
1539 help_url = (
1540 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1541 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001542 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1543 if i:
1544 print('')
maruel77f720b2015-09-15 12:35:22 -07001545 print api_id
maruel11e31af2017-02-15 07:30:50 -08001546 print ' ' + api['description'].strip()
1547 if 'resources' in api:
1548 # Old.
1549 for j, (resource_name, resource) in enumerate(
1550 sorted(api['resources'].iteritems())):
1551 if j:
1552 print('')
1553 for method_name, method in sorted(resource['methods'].iteritems()):
1554 # Only list the GET ones.
1555 if method['httpMethod'] != 'GET':
1556 continue
1557 print '- %s.%s: %s' % (
1558 resource_name, method_name, method['path'])
1559 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001560 ' ' + l for l in textwrap.wrap(
1561 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001562 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1563 else:
1564 # New.
1565 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001566 # Only list the GET ones.
1567 if method['httpMethod'] != 'GET':
1568 continue
maruel11e31af2017-02-15 07:30:50 -08001569 print '- %s: %s' % (method['id'], method['path'])
1570 print('\n'.join(
1571 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001572 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1573 return 0
1574
1575
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001576@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001577def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001578 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001579
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001580 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001581 """
1582 add_trigger_options(parser)
1583 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001584 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001585 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001586 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001587 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001588 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001589 tasks = trigger_task_shards(
1590 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001591 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001592 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001593 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001594 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001595 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001596 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001597 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001598 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001599 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001600 task_ids = [
1601 t['task_id']
1602 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1603 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001604 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001605 offset = 0
1606 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001607 m = (offset + s.properties.execution_timeout_secs +
1608 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001609 if m > options.timeout:
1610 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001611 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001612 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001613 try:
1614 return collect(
1615 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001616 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001617 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001618 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001619 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001620 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001621 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001622 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001623 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001624 except Failure:
1625 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001626 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001627
1628
maruel18122c62015-10-23 06:31:23 -07001629@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001630def CMDreproduce(parser, args):
1631 """Runs a task locally that was triggered on the server.
1632
1633 This running locally the same commands that have been run on the bot. The data
1634 downloaded will be in a subdirectory named 'work' of the current working
1635 directory.
maruel18122c62015-10-23 06:31:23 -07001636
1637 You can pass further additional arguments to the target command by passing
1638 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001639 """
maruelc070e672016-02-22 17:32:57 -08001640 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001641 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001642 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001643 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001644 extra_args = []
1645 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001646 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001647 if len(args) > 1:
1648 if args[1] == '--':
1649 if len(args) > 2:
1650 extra_args = args[2:]
1651 else:
1652 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001653
smut281c3902018-05-30 17:50:05 -07001654 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001655 request = net.url_read_json(url)
1656 if not request:
1657 print >> sys.stderr, 'Failed to retrieve request data for the task'
1658 return 1
1659
maruel12e30012015-10-09 11:55:35 -07001660 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001661 if fs.isdir(workdir):
1662 parser.error('Please delete the directory \'work\' first')
1663 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001664 cachedir = unicode(os.path.abspath('cipd_cache'))
1665 if not fs.exists(cachedir):
1666 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001667
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001668 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001669 env = os.environ.copy()
1670 env['SWARMING_BOT_ID'] = 'reproduce'
1671 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001672 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001673 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001674 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001675 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001676 if not i['value']:
1677 env.pop(key, None)
1678 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001679 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001680
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001681 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001682 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001683 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001684 for i in env_prefixes:
1685 key = i['key']
1686 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001687 cur = env.get(key)
1688 if cur:
1689 paths.append(cur)
1690 env[key] = os.path.pathsep.join(paths)
1691
iannucci31ab9192017-05-02 19:11:56 -07001692 command = []
nodir152cba62016-05-12 16:08:56 -07001693 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001694 # Create the tree.
1695 with isolateserver.get_storage(
1696 properties['inputs_ref']['isolatedserver'],
1697 properties['inputs_ref']['namespace']) as storage:
1698 bundle = isolateserver.fetch_isolated(
1699 properties['inputs_ref']['isolated'],
1700 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -04001701 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001702 workdir,
1703 False)
maruel29ab2fd2015-10-16 11:44:01 -07001704 command = bundle.command
1705 if bundle.relative_cwd:
1706 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001707 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001708
1709 if properties.get('command'):
1710 command.extend(properties['command'])
1711
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001712 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001713 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001714 if not options.output_dir:
1715 new_command = run_isolated.process_command(command, 'invalid', None)
1716 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001717 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001718 else:
1719 # Make the path absolute, as the process will run from a subdirectory.
1720 options.output_dir = os.path.abspath(options.output_dir)
1721 new_command = run_isolated.process_command(
1722 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001723 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001724 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001725 command = new_command
1726 file_path.ensure_command_has_abs_path(command, workdir)
1727
1728 if properties.get('cipd_input'):
1729 ci = properties['cipd_input']
1730 cp = ci['client_package']
1731 client_manager = cipd.get_client(
1732 ci['server'], cp['package_name'], cp['version'], cachedir)
1733
1734 with client_manager as client:
1735 by_path = collections.defaultdict(list)
1736 for pkg in ci['packages']:
1737 path = pkg['path']
1738 # cipd deals with 'root' as ''
1739 if path == '.':
1740 path = ''
1741 by_path[path].append((pkg['package_name'], pkg['version']))
1742 client.ensure(workdir, by_path, cache_dir=cachedir)
1743
maruel77f720b2015-09-15 12:35:22 -07001744 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001745 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001746 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001747 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001748 print >> sys.stderr, str(e)
1749 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001750
1751
maruel0eb1d1b2015-10-02 14:48:21 -07001752@subcommand.usage('bot_id')
1753def CMDterminate(parser, args):
1754 """Tells a bot to gracefully shut itself down as soon as it can.
1755
1756 This is done by completing whatever current task there is then exiting the bot
1757 process.
1758 """
1759 parser.add_option(
1760 '--wait', action='store_true', help='Wait for the bot to terminate')
1761 options, args = parser.parse_args(args)
1762 if len(args) != 1:
1763 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001764 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001765 request = net.url_read_json(url, data={})
1766 if not request:
1767 print >> sys.stderr, 'Failed to ask for termination'
1768 return 1
1769 if options.wait:
1770 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001771 options.swarming,
1772 [request['task_id']],
1773 0.,
1774 False,
1775 False,
1776 None,
1777 None,
1778 [],
maruel9531ce02016-04-13 06:11:23 -07001779 False)
maruelbfc5f872017-06-10 16:43:17 -07001780 else:
1781 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001782 return 0
1783
1784
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001785@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001786def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001787 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001788
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001789 Passes all extra arguments provided after '--' as additional command line
1790 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001791 """
1792 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001793 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001794 parser.add_option(
1795 '--dump-json',
1796 metavar='FILE',
1797 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001798 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001799 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001800 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001801 tasks = trigger_task_shards(
1802 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001803 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001804 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001805 tasks_sorted = sorted(
1806 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001807 if options.dump_json:
1808 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001809 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001810 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001811 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001812 }
maruel46b015f2015-10-13 18:40:35 -07001813 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001814 print('To collect results, use:')
1815 print(' swarming.py collect -S %s --json %s' %
1816 (options.swarming, options.dump_json))
1817 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001818 print('To collect results, use:')
1819 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001820 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1821 print('Or visit:')
1822 for t in tasks_sorted:
1823 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001824 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001825 except Failure:
1826 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001827 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001828
1829
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001830class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001831 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001832 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001833 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001834 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001835 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001836 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001837 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001838 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001839 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001840 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001841
1842 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001843 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001844 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001845 auth.process_auth_options(self, options)
1846 user = self._process_swarming(options)
1847 if hasattr(options, 'user') and not options.user:
1848 options.user = user
1849 return options, args
1850
1851 def _process_swarming(self, options):
1852 """Processes the --swarming option and aborts if not specified.
1853
1854 Returns the identity as determined by the server.
1855 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001856 if not options.swarming:
1857 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001858 try:
1859 options.swarming = net.fix_url(options.swarming)
1860 except ValueError as e:
1861 self.error('--swarming %s' % e)
1862 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001863 try:
1864 user = auth.ensure_logged_in(options.swarming)
1865 except ValueError as e:
1866 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001867 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001868
1869
1870def main(args):
1871 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001872 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001873
1874
1875if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001876 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001877 fix_encoding.fix_encoding()
1878 tools.disable_buffering()
1879 colorama.init()
1880 sys.exit(main(sys.argv[1:]))