blob: 65e82d0a5ed51059f87daba5988a105084f95115 [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
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -04008__version__ = '0.12'
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
maruelc070e672016-02-22 17:32:57 -080040import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000041
42
tansella4949442016-06-23 22:34:32 -070043ROOT_DIR = os.path.dirname(os.path.abspath(
44 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050045
46
47class Failure(Exception):
48 """Generic failure."""
49 pass
50
51
maruel0a25f6c2017-05-10 10:43:23 -070052def default_task_name(options):
53 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050054 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070055 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070056 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070057 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070058 if options.isolated:
59 task_name += u'/' + options.isolated
60 return task_name
61 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050062
63
64### Triggering.
65
66
maruel77f720b2015-09-15 12:35:22 -070067# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070068CipdPackage = collections.namedtuple(
69 'CipdPackage',
70 [
71 'package_name',
72 'path',
73 'version',
74 ])
75
76
77# See ../appengine/swarming/swarming_rpcs.py.
78CipdInput = collections.namedtuple(
79 'CipdInput',
80 [
81 'client_package',
82 'packages',
83 'server',
84 ])
85
86
87# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070088FilesRef = collections.namedtuple(
89 'FilesRef',
90 [
91 'isolated',
92 'isolatedserver',
93 'namespace',
94 ])
95
96
97# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -080098StringListPair = collections.namedtuple(
99 'StringListPair', [
100 'key',
101 'value', # repeated string
102 ]
103)
104
105
106# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700107TaskProperties = collections.namedtuple(
108 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500109 [
maruel681d6802017-01-17 16:56:03 -0800110 'caches',
borenet02f772b2016-06-22 12:42:19 -0700111 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500112 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500113 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500114 'dimensions',
115 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800116 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700117 'execution_timeout_secs',
118 'extra_args',
119 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500120 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700121 'inputs_ref',
122 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700123 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700124 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700125 ])
126
127
128# See ../appengine/swarming/swarming_rpcs.py.
129NewTaskRequest = collections.namedtuple(
130 'NewTaskRequest',
131 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500132 'name',
maruel77f720b2015-09-15 12:35:22 -0700133 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500134 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400135 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700136 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500137 'tags',
138 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500139 ])
140
141
maruel77f720b2015-09-15 12:35:22 -0700142def namedtuple_to_dict(value):
143 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400144 if hasattr(value, '_asdict'):
145 return namedtuple_to_dict(value._asdict())
146 if isinstance(value, (list, tuple)):
147 return [namedtuple_to_dict(v) for v in value]
148 if isinstance(value, dict):
149 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
150 return value
maruel77f720b2015-09-15 12:35:22 -0700151
152
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700153def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800154 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700155
156 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500157 """
maruel77f720b2015-09-15 12:35:22 -0700158 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700159 # Don't send 'service_account' if it is None to avoid confusing older
160 # version of the server that doesn't know about 'service_account' and don't
161 # use it at all.
162 if not out['service_account']:
163 out.pop('service_account')
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400164 out['task_slices'][0]['properties']['dimensions'] = [
maruel77f720b2015-09-15 12:35:22 -0700165 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400166 for k, v in out['task_slices'][0]['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700167 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400168 out['task_slices'][0]['properties']['env'] = [
maruel77f720b2015-09-15 12:35:22 -0700169 {'key': k, 'value': v}
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400170 for k, v in out['task_slices'][0]['properties']['env'].iteritems()
maruel77f720b2015-09-15 12:35:22 -0700171 ]
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400172 out['task_slices'][0]['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700173 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500174
175
maruel77f720b2015-09-15 12:35:22 -0700176def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500177 """Triggers a request on the Swarming server and returns the json data.
178
179 It's the low-level function.
180
181 Returns:
182 {
183 'request': {
184 'created_ts': u'2010-01-02 03:04:05',
185 'name': ..
186 },
187 'task_id': '12300',
188 }
189 """
190 logging.info('Triggering: %s', raw_request['name'])
191
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500192 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700193 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500194 if not result:
195 on_error.report('Failed to trigger task %s' % raw_request['name'])
196 return None
maruele557bce2015-11-17 09:01:27 -0800197 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800198 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800199 msg = 'Failed to trigger task %s' % raw_request['name']
200 if result['error'].get('errors'):
201 for err in result['error']['errors']:
202 if err.get('message'):
203 msg += '\nMessage: %s' % err['message']
204 if err.get('debugInfo'):
205 msg += '\nDebug info:\n%s' % err['debugInfo']
206 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800207 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800208
209 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800210 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500211 return result
212
213
214def setup_googletest(env, shards, index):
215 """Sets googletest specific environment variables."""
216 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700217 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
218 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
219 env = env[:]
220 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
221 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500222 return env
223
224
225def trigger_task_shards(swarming, task_request, shards):
226 """Triggers one or many subtasks of a sharded task.
227
228 Returns:
229 Dict with task details, returned to caller as part of --dump-json output.
230 None in case of failure.
231 """
232 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700233 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500234 if shards > 1:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400235 req['task_slices'][0]['properties']['env'] = setup_googletest(
236 req['task_slices'][0]['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700237 req['name'] += ':%s:%s' % (index, shards)
238 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500239
240 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500241 tasks = {}
242 priority_warning = False
243 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700244 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500245 if not task:
246 break
247 logging.info('Request result: %s', task)
248 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400249 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500250 priority_warning = True
251 print >> sys.stderr, (
252 'Priority was reset to %s' % task['request']['priority'])
253 tasks[request['name']] = {
254 'shard_index': index,
255 'task_id': task['task_id'],
256 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
257 }
258
259 # Some shards weren't triggered. Abort everything.
260 if len(tasks) != len(requests):
261 if tasks:
262 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
263 len(tasks), len(requests))
264 for task_dict in tasks.itervalues():
265 abort_task(swarming, task_dict['task_id'])
266 return None
267
268 return tasks
269
270
271### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000272
273
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700274# How often to print status updates to stdout in 'collect'.
275STATUS_UPDATE_INTERVAL = 15 * 60.
276
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400277
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400278class State(object):
279 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000280
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400281 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
282 values are part of the API so if they change, the API changed.
283
284 It's in fact an enum. Values should be in decreasing order of importance.
285 """
286 RUNNING = 0x10
287 PENDING = 0x20
288 EXPIRED = 0x30
289 TIMED_OUT = 0x40
290 BOT_DIED = 0x50
291 CANCELED = 0x60
292 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400293 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400294 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400295
maruel77f720b2015-09-15 12:35:22 -0700296 STATES = (
297 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400298 'COMPLETED', 'KILLED', 'NO_RESOURCE')
maruel77f720b2015-09-15 12:35:22 -0700299 STATES_RUNNING = ('RUNNING', 'PENDING')
300 STATES_NOT_RUNNING = (
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400301 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED', 'KILLED',
302 'NO_RESOURCE')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400303 STATES_DONE = ('TIMED_OUT', 'COMPLETED', 'KILLED')
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400304 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED', 'NO_RESOURCE')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400305
306 _NAMES = {
307 RUNNING: 'Running',
308 PENDING: 'Pending',
309 EXPIRED: 'Expired',
310 TIMED_OUT: 'Execution timed out',
311 BOT_DIED: 'Bot died',
312 CANCELED: 'User canceled',
313 COMPLETED: 'Completed',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400314 KILLED: 'User killed',
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400315 NO_RESOURCE: 'No resource',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400316 }
317
maruel77f720b2015-09-15 12:35:22 -0700318 _ENUMS = {
319 'RUNNING': RUNNING,
320 'PENDING': PENDING,
321 'EXPIRED': EXPIRED,
322 'TIMED_OUT': TIMED_OUT,
323 'BOT_DIED': BOT_DIED,
324 'CANCELED': CANCELED,
325 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400326 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400327 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700328 }
329
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400330 @classmethod
331 def to_string(cls, state):
332 """Returns a user-readable string representing a State."""
333 if state not in cls._NAMES:
334 raise ValueError('Invalid state %s' % state)
335 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000336
maruel77f720b2015-09-15 12:35:22 -0700337 @classmethod
338 def from_enum(cls, state):
339 """Returns int value based on the string."""
340 if state not in cls._ENUMS:
341 raise ValueError('Invalid state %s' % state)
342 return cls._ENUMS[state]
343
maruel@chromium.org0437a732013-08-27 16:05:52 +0000344
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700345class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700346 """Assembles task execution summary (for --task-summary-json output).
347
348 Optionally fetches task outputs from isolate server to local disk (used when
349 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700350
351 This object is shared among multiple threads running 'retrieve_results'
352 function, in particular they call 'process_shard_result' method in parallel.
353 """
354
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000355 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700356 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
357
358 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700359 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360 shard_count: expected number of task shards.
361 """
maruel12e30012015-10-09 11:55:35 -0700362 self.task_output_dir = (
363 unicode(os.path.abspath(task_output_dir))
364 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000365 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700366 self.shard_count = shard_count
367
368 self._lock = threading.Lock()
369 self._per_shard_results = {}
370 self._storage = None
371
nodire5028a92016-04-29 14:38:21 -0700372 if self.task_output_dir:
373 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374
Vadim Shtayurab450c602014-05-12 19:23:25 -0700375 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376 """Stores results of a single task shard, fetches output files if necessary.
377
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400378 Modifies |result| in place.
379
maruel77f720b2015-09-15 12:35:22 -0700380 shard_index is 0-based.
381
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 Called concurrently from multiple threads.
383 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700385 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 if shard_index < 0 or shard_index >= self.shard_count:
387 logging.warning(
388 'Shard index %d is outside of expected range: [0; %d]',
389 shard_index, self.shard_count - 1)
390 return
391
maruel77f720b2015-09-15 12:35:22 -0700392 if result.get('outputs_ref'):
393 ref = result['outputs_ref']
394 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
395 ref['isolatedserver'],
396 urllib.urlencode(
397 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400398
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399 # Store result dict of that shard, ignore results we've already seen.
400 with self._lock:
401 if shard_index in self._per_shard_results:
402 logging.warning('Ignoring duplicate shard index %d', shard_index)
403 return
404 self._per_shard_results[shard_index] = result
405
406 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700407 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400408 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700409 result['outputs_ref']['isolatedserver'],
410 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400411 if storage:
412 # Output files are supposed to be small and they are not reused across
413 # tasks. So use MemoryCache for them instead of on-disk cache. Make
414 # files writable, so that calling script can delete them.
415 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700416 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400417 storage,
418 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700419 os.path.join(self.task_output_dir, str(shard_index)),
420 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700421
422 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700423 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424 with self._lock:
425 # Write an array of shard results with None for missing shards.
426 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427 'shards': [
428 self._per_shard_results.get(i) for i in xrange(self.shard_count)
429 ],
430 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000431
432 # Don't store stdout in the summary if not requested too.
433 if "json" not in self.task_output_stdout:
434 for shard_json in summary['shards']:
435 if not shard_json:
436 continue
437 if "output" in shard_json:
438 del shard_json["output"]
439 if "outputs" in shard_json:
440 del shard_json["outputs"]
441
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 # Write summary.json to task_output_dir as well.
443 if self.task_output_dir:
444 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700445 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700446 summary,
447 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 if self._storage:
449 self._storage.close()
450 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700451 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452
453 def _get_storage(self, isolate_server, namespace):
454 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700455 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 with self._lock:
457 if not self._storage:
458 self._storage = isolateserver.get_storage(isolate_server, namespace)
459 else:
460 # Shards must all use exact same isolate server and namespace.
461 if self._storage.location != isolate_server:
462 logging.error(
463 'Task shards are using multiple isolate servers: %s and %s',
464 self._storage.location, isolate_server)
465 return None
466 if self._storage.namespace != namespace:
467 logging.error(
468 'Task shards are using multiple namespaces: %s and %s',
469 self._storage.namespace, namespace)
470 return None
471 return self._storage
472
473
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500474def now():
475 """Exists so it can be mocked easily."""
476 return time.time()
477
478
maruel77f720b2015-09-15 12:35:22 -0700479def parse_time(value):
480 """Converts serialized time from the API to datetime.datetime."""
481 # When microseconds are 0, the '.123456' suffix is elided. This means the
482 # serialized format is not consistent, which confuses the hell out of python.
483 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
484 try:
485 return datetime.datetime.strptime(value, fmt)
486 except ValueError:
487 pass
488 raise ValueError('Failed to parse %s' % value)
489
490
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700491def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700492 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000493 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400494 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700495
Vadim Shtayurab450c602014-05-12 19:23:25 -0700496 Returns:
497 <result dict> on success.
498 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700499 """
maruel71c61c82016-02-22 06:52:05 -0800500 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700501 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700502 if include_perf:
503 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700504 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700505 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400506 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700507 attempt = 0
508
509 while not should_stop.is_set():
510 attempt += 1
511
512 # Waiting for too long -> give up.
513 current_time = now()
514 if deadline and current_time >= deadline:
515 logging.error('retrieve_results(%s) timed out on attempt %d',
516 base_url, attempt)
517 return None
518
519 # Do not spin too fast. Spin faster at the beginning though.
520 # Start with 1 sec delay and for each 30 sec of waiting add another second
521 # of delay, until hitting 15 sec ceiling.
522 if attempt > 1:
523 max_delay = min(15, 1 + (current_time - started) / 30.0)
524 delay = min(max_delay, deadline - current_time) if deadline else max_delay
525 if delay > 0:
526 logging.debug('Waiting %.1f sec before retrying', delay)
527 should_stop.wait(delay)
528 if should_stop.is_set():
529 return None
530
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 # Disable internal retries in net.url_read_json, since we are doing retries
532 # ourselves.
533 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700534 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
535 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400536 # Retry on 500s only if no timeout is specified.
537 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400538 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400539 if timeout == -1:
540 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400541 continue
maruel77f720b2015-09-15 12:35:22 -0700542
maruelbf53e042015-12-01 15:00:51 -0800543 if result.get('error'):
544 # An error occurred.
545 if result['error'].get('errors'):
546 for err in result['error']['errors']:
547 logging.warning(
548 'Error while reading task: %s; %s',
549 err.get('message'), err.get('debugInfo'))
550 elif result['error'].get('message'):
551 logging.warning(
552 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400553 if timeout == -1:
554 return result
maruelbf53e042015-12-01 15:00:51 -0800555 continue
556
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400557 # When timeout == -1, always return on first attempt. 500s are already
558 # retried in this case.
559 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000560 if fetch_stdout:
561 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700562 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700563 # Record the result, try to fetch attached output files (if any).
564 if output_collector:
565 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700566 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700567 if result.get('internal_failure'):
568 logging.error('Internal error!')
569 elif result['state'] == 'BOT_DIED':
570 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700571 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000572
573
maruel77f720b2015-09-15 12:35:22 -0700574def convert_to_old_format(result):
575 """Converts the task result data from Endpoints API format to old API format
576 for compatibility.
577
578 This goes into the file generated as --task-summary-json.
579 """
580 # Sets default.
581 result.setdefault('abandoned_ts', None)
582 result.setdefault('bot_id', None)
583 result.setdefault('bot_version', None)
584 result.setdefault('children_task_ids', [])
585 result.setdefault('completed_ts', None)
586 result.setdefault('cost_saved_usd', None)
587 result.setdefault('costs_usd', None)
588 result.setdefault('deduped_from', None)
589 result.setdefault('name', None)
590 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700591 result.setdefault('server_versions', None)
592 result.setdefault('started_ts', None)
593 result.setdefault('tags', None)
594 result.setdefault('user', None)
595
596 # Convertion back to old API.
597 duration = result.pop('duration', None)
598 result['durations'] = [duration] if duration else []
599 exit_code = result.pop('exit_code', None)
600 result['exit_codes'] = [int(exit_code)] if exit_code else []
601 result['id'] = result.pop('task_id')
602 result['isolated_out'] = result.get('outputs_ref', None)
603 output = result.pop('output', None)
604 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700605 # server_version
606 # Endpoints result 'state' as string. For compatibility with old code, convert
607 # to int.
608 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700609 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700610 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700611 if 'bot_dimensions' in result:
612 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700613 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700614 }
615 else:
616 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700617
618
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700619def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400620 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000621 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500622 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000623
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700624 Duplicate shards are ignored. Shards are yielded in order of completion.
625 Timed out shards are NOT yielded at all. Caller can compare number of yielded
626 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000627
628 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500629 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 +0000630 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500631
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700632 output_collector is an optional instance of TaskOutputCollector that will be
633 used to fetch files produced by a task from isolate server to the local disk.
634
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500635 Yields:
636 (index, result). In particular, 'result' is defined as the
637 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000638 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400640 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700641 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700642 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643
maruel@chromium.org0437a732013-08-27 16:05:52 +0000644 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
645 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700646 # Adds a task to the thread pool to call 'retrieve_results' and return
647 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400648 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700649 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400651 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000652 task_id, timeout, should_stop, output_collector, include_perf,
653 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654
655 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400656 for shard_index, task_id in enumerate(task_ids):
657 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700658
659 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400660 shards_remaining = range(len(task_ids))
661 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700662 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700663 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700664 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700665 shard_index, result = results_channel.pull(
666 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700667 except threading_utils.TaskChannel.Timeout:
668 if print_status_updates:
669 print(
670 'Waiting for results from the following shards: %s' %
671 ', '.join(map(str, shards_remaining)))
672 sys.stdout.flush()
673 continue
674 except Exception:
675 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700676
677 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500680 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700682
Vadim Shtayurab450c602014-05-12 19:23:25 -0700683 # Yield back results to the caller.
684 assert shard_index in shards_remaining
685 shards_remaining.remove(shard_index)
686 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700687
maruel@chromium.org0437a732013-08-27 16:05:52 +0000688 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700689 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 should_stop.set()
691
692
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000693def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000694 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700695 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400696 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700697 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
698 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400699 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
700 metadata.get('abandoned_ts')):
701 pending = '%.1fs' % (
702 parse_time(metadata['abandoned_ts']) -
703 parse_time(metadata['created_ts'])
704 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400705 else:
706 pending = 'N/A'
707
maruel77f720b2015-09-15 12:35:22 -0700708 if metadata.get('duration') is not None:
709 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400710 else:
711 duration = 'N/A'
712
maruel77f720b2015-09-15 12:35:22 -0700713 if metadata.get('exit_code') is not None:
714 # Integers are encoded as string to not loose precision.
715 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400716 else:
717 exit_code = 'N/A'
718
719 bot_id = metadata.get('bot_id') or 'N/A'
720
maruel77f720b2015-09-15 12:35:22 -0700721 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400722 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000723 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400724 if metadata.get('state') == 'CANCELED':
725 tag_footer2 = ' Pending: %s CANCELED' % pending
726 elif metadata.get('state') == 'EXPIRED':
727 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400728 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400729 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
730 pending, duration, bot_id, exit_code, metadata['state'])
731 else:
732 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
733 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400734
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000735 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
736 dash_pad = '+-%s-+' % ('-' * tag_len)
737 tag_header = '| %s |' % tag_header.ljust(tag_len)
738 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
739 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400740
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000741 if include_stdout:
742 return '\n'.join([
743 dash_pad,
744 tag_header,
745 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400746 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000747 dash_pad,
748 tag_footer1,
749 tag_footer2,
750 dash_pad,
751 ])
752 else:
753 return '\n'.join([
754 dash_pad,
755 tag_header,
756 tag_footer2,
757 dash_pad,
758 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000759
760
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700761def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700762 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000763 task_summary_json, task_output_dir, task_output_stdout,
764 include_perf):
maruela5490782015-09-30 10:56:59 -0700765 """Retrieves results of a Swarming task.
766
767 Returns:
768 process exit code that should be returned to the user.
769 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700770 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000771 output_collector = TaskOutputCollector(
772 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700774 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700775 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400776 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700777 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400778 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400779 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000780 output_collector, include_perf,
781 (len(task_output_stdout) > 0),
782 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700783 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700784
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400785 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700786 shard_exit_code = metadata.get('exit_code')
787 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700788 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700789 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700790 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400791 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700792 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700793
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700794 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000795 s = decorate_shard_output(
796 swarming, index, metadata,
797 "console" in task_output_stdout).encode(
798 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700799 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400800 if len(seen_shards) < len(task_ids):
801 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700802 else:
maruel77f720b2015-09-15 12:35:22 -0700803 print('%s: %s %s' % (
804 metadata.get('bot_id', 'N/A'),
805 metadata['task_id'],
806 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000807 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700808 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400809 if output:
810 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700811 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700812 summary = output_collector.finalize()
813 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700814 # TODO(maruel): Make this optional.
815 for i in summary['shards']:
816 if i:
817 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700818 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700819
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400820 if decorate and total_duration:
821 print('Total duration: %.1fs' % total_duration)
822
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400823 if len(seen_shards) != len(task_ids):
824 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700825 print >> sys.stderr, ('Results from some shards are missing: %s' %
826 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700827 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700828
maruela5490782015-09-30 10:56:59 -0700829 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000830
831
maruel77f720b2015-09-15 12:35:22 -0700832### API management.
833
834
835class APIError(Exception):
836 pass
837
838
839def endpoints_api_discovery_apis(host):
840 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
841 the APIs exposed by a host.
842
843 https://developers.google.com/discovery/v1/reference/apis/list
844 """
maruel380e3262016-08-31 16:10:06 -0700845 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
846 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700847 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
848 if data is None:
849 raise APIError('Failed to discover APIs on %s' % host)
850 out = {}
851 for api in data['items']:
852 if api['id'] == 'discovery:v1':
853 continue
854 # URL is of the following form:
855 # url = host + (
856 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
857 api_data = net.url_read_json(api['discoveryRestUrl'])
858 if api_data is None:
859 raise APIError('Failed to discover %s on %s' % (api['id'], host))
860 out[api['id']] = api_data
861 return out
862
863
maruelaf6b06c2017-06-08 06:26:53 -0700864def get_yielder(base_url, limit):
865 """Returns the first query and a function that yields following items."""
866 CHUNK_SIZE = 250
867
868 url = base_url
869 if limit:
870 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
871 data = net.url_read_json(url)
872 if data is None:
873 # TODO(maruel): Do basic diagnostic.
874 raise Failure('Failed to access %s' % url)
875 org_cursor = data.pop('cursor', None)
876 org_total = len(data.get('items') or [])
877 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
878 if not org_cursor or not org_total:
879 # This is not an iterable resource.
880 return data, lambda: []
881
882 def yielder():
883 cursor = org_cursor
884 total = org_total
885 # Some items support cursors. Try to get automatically if cursors are needed
886 # by looking at the 'cursor' items.
887 while cursor and (not limit or total < limit):
888 merge_char = '&' if '?' in base_url else '?'
889 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
890 if limit:
891 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
892 new = net.url_read_json(url)
893 if new is None:
894 raise Failure('Failed to access %s' % url)
895 cursor = new.get('cursor')
896 new_items = new.get('items')
897 nb_items = len(new_items or [])
898 total += nb_items
899 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
900 yield new_items
901
902 return data, yielder
903
904
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500905### Commands.
906
907
908def abort_task(_swarming, _manifest):
909 """Given a task manifest that was triggered, aborts its execution."""
910 # TODO(vadimsh): No supported by the server yet.
911
912
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400913def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800914 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500915 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500916 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500917 dest='dimensions', metavar='FOO bar',
918 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500919 parser.add_option_group(parser.filter_group)
920
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400921
maruelaf6b06c2017-06-08 06:26:53 -0700922def process_filter_options(parser, options):
923 for key, value in options.dimensions:
924 if ':' in key:
925 parser.error('--dimension key cannot contain ":"')
926 if key.strip() != key:
927 parser.error('--dimension key has whitespace')
928 if not key:
929 parser.error('--dimension key is empty')
930
931 if value.strip() != value:
932 parser.error('--dimension value has whitespace')
933 if not value:
934 parser.error('--dimension value is empty')
935 options.dimensions.sort()
936
937
Vadim Shtayurab450c602014-05-12 19:23:25 -0700938def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400939 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700940 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700941 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700942 help='Number of shards to trigger and collect.')
943 parser.add_option_group(parser.sharding_group)
944
945
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400946def add_trigger_options(parser):
947 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500948 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400949 add_filter_options(parser)
950
maruel681d6802017-01-17 16:56:03 -0800951 group = optparse.OptionGroup(parser, 'Task properties')
952 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700953 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500954 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800955 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500956 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700957 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800958 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800959 '--env-prefix', default=[], action='append', nargs=2,
960 metavar='VAR local/path',
961 help='Prepend task-relative `local/path` to the task\'s VAR environment '
962 'variable using os-appropriate pathsep character. Can be specified '
963 'multiple times for the same VAR to add multiple paths.')
964 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400965 '--idempotent', action='store_true', default=False,
966 help='When set, the server will actively try to find a previous task '
967 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800968 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700969 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700970 help='The optional path to a file containing the secret_bytes to use with'
971 'this task.')
maruel681d6802017-01-17 16:56:03 -0800972 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700973 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400974 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800975 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700976 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400977 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800978 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500979 '--raw-cmd', action='store_true', default=False,
980 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700981 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800982 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500983 '--relative-cwd',
984 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
985 'requires --raw-cmd')
986 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700987 '--cipd-package', action='append', default=[], metavar='PKG',
988 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700989 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800990 group.add_option(
991 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700992 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800993 help='"<name> <relpath>" items to keep a persistent bot managed cache')
994 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700995 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700996 help='Email of a service account to run the task as, or literal "bot" '
997 'string to indicate that the task should use the same account the '
998 'bot itself is using to authenticate to Swarming. Don\'t use task '
999 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001000 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001001 '-o', '--output', action='append', default=[], metavar='PATH',
1002 help='A list of files to return in addition to those written to '
1003 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1004 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -08001005 parser.add_option_group(group)
1006
1007 group = optparse.OptionGroup(parser, 'Task request')
1008 group.add_option(
1009 '--priority', type='int', default=100,
1010 help='The lower value, the more important the task is')
1011 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001012 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001013 help='Display name of the task. Defaults to '
1014 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1015 'isolated file is provided, if a hash is provided, it defaults to '
1016 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1017 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001018 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001019 help='Tags to assign to the task.')
1020 group.add_option(
1021 '--user', default='',
1022 help='User associated with the task. Defaults to authenticated user on '
1023 'the server.')
1024 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001025 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001026 help='Seconds to allow the task to be pending for a bot to run before '
1027 'this task request expires.')
1028 group.add_option(
1029 '--deadline', type='int', dest='expiration',
1030 help=optparse.SUPPRESS_HELP)
1031 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001032
1033
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001034def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001035 """Processes trigger options and does preparatory steps.
1036
1037 Returns:
1038 NewTaskRequest instance.
1039 """
maruelaf6b06c2017-06-08 06:26:53 -07001040 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001041 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001042 if args and args[0] == '--':
1043 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001044
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001045 if not options.dimensions:
1046 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001047 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1048 parser.error('--tags must be in the format key:value')
1049 if options.raw_cmd and not args:
1050 parser.error(
1051 'Arguments with --raw-cmd should be passed after -- as command '
1052 'delimiter.')
1053 if options.isolate_server and not options.namespace:
1054 parser.error(
1055 '--namespace must be a valid value when --isolate-server is used')
1056 if not options.isolated and not options.raw_cmd:
1057 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1058
1059 # Isolated
1060 # --isolated is required only if --raw-cmd wasn't provided.
1061 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1062 # preferred server.
1063 isolateserver.process_isolate_server_options(
1064 parser, options, False, not options.raw_cmd)
1065 inputs_ref = None
1066 if options.isolate_server:
1067 inputs_ref = FilesRef(
1068 isolated=options.isolated,
1069 isolatedserver=options.isolate_server,
1070 namespace=options.namespace)
1071
1072 # Command
1073 command = None
1074 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001075 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001076 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001077 if options.relative_cwd:
1078 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1079 if not a.startswith(os.getcwd()):
1080 parser.error(
1081 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001082 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001083 if options.relative_cwd:
1084 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001085 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001086
maruel0a25f6c2017-05-10 10:43:23 -07001087 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001088 cipd_packages = []
1089 for p in options.cipd_package:
1090 split = p.split(':', 2)
1091 if len(split) != 3:
1092 parser.error('CIPD packages must take the form: path:package:version')
1093 cipd_packages.append(CipdPackage(
1094 package_name=split[1],
1095 path=split[0],
1096 version=split[2]))
1097 cipd_input = None
1098 if cipd_packages:
1099 cipd_input = CipdInput(
1100 client_package=None,
1101 packages=cipd_packages,
1102 server=None)
1103
maruel0a25f6c2017-05-10 10:43:23 -07001104 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001105 secret_bytes = None
1106 if options.secret_bytes_path:
1107 with open(options.secret_bytes_path, 'r') as f:
1108 secret_bytes = f.read().encode('base64')
1109
maruel0a25f6c2017-05-10 10:43:23 -07001110 # Named caches
maruel681d6802017-01-17 16:56:03 -08001111 caches = [
1112 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1113 for i in options.named_cache
1114 ]
maruel0a25f6c2017-05-10 10:43:23 -07001115
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001116 env_prefixes = {}
1117 for k, v in options.env_prefix:
1118 env_prefixes.setdefault(k, []).append(v)
1119
maruel77f720b2015-09-15 12:35:22 -07001120 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001121 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001122 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001123 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001124 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001125 dimensions=options.dimensions,
1126 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001127 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001128 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001129 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001130 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001131 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001132 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001133 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001134 outputs=options.output,
1135 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001136
maruel77f720b2015-09-15 12:35:22 -07001137 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001138 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001139 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001140 priority=options.priority,
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001141 task_slices=[
1142 {
1143 'expiration_secs': options.expiration,
1144 'properties': properties,
1145 },
1146 ],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001147 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001148 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001149 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001150
1151
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001152class TaskOutputStdoutOption(optparse.Option):
1153 """Where to output the each task's console output (stderr/stdout).
1154
1155 The output will be;
1156 none - not be downloaded.
1157 json - stored in summary.json file *only*.
1158 console - shown on stdout *only*.
1159 all - stored in summary.json and shown on stdout.
1160 """
1161
1162 choices = ['all', 'json', 'console', 'none']
1163
1164 def __init__(self, *args, **kw):
1165 optparse.Option.__init__(
1166 self,
1167 *args,
1168 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001169 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001170 help=re.sub('\s\s*', ' ', self.__doc__),
1171 **kw)
1172
1173 def convert_value(self, opt, value):
1174 if value not in self.choices:
1175 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1176 self.get_opt_string(), self.choices, value))
1177 stdout_to = []
1178 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001179 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001180 elif value != 'none':
1181 stdout_to = [value]
1182 return stdout_to
1183
1184
maruel@chromium.org0437a732013-08-27 16:05:52 +00001185def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001186 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001187 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001188 help='Timeout to wait for result, set to -1 for no timeout and get '
1189 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001190 parser.group_logging.add_option(
1191 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001192 parser.group_logging.add_option(
1193 '--print-status-updates', action='store_true',
1194 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001195 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001196 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001197 '--task-summary-json',
1198 metavar='FILE',
1199 help='Dump a summary of task results to this file as json. It contains '
1200 'only shards statuses as know to server directly. Any output files '
1201 'emitted by the task can be collected by using --task-output-dir')
1202 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001203 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001204 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001205 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001206 'directory contains per-shard directory with output files produced '
1207 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001208 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001209 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001210 parser.task_output_group.add_option(
1211 '--perf', action='store_true', default=False,
1212 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001213 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001214
1215
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001216def process_collect_options(parser, options):
1217 # Only negative -1 is allowed, disallow other negative values.
1218 if options.timeout != -1 and options.timeout < 0:
1219 parser.error('Invalid --timeout value')
1220
1221
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001222@subcommand.usage('bots...')
1223def CMDbot_delete(parser, args):
1224 """Forcibly deletes bots from the Swarming server."""
1225 parser.add_option(
1226 '-f', '--force', action='store_true',
1227 help='Do not prompt for confirmation')
1228 options, args = parser.parse_args(args)
1229 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001230 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001231
1232 bots = sorted(args)
1233 if not options.force:
1234 print('Delete the following bots?')
1235 for bot in bots:
1236 print(' %s' % bot)
1237 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1238 print('Goodbye.')
1239 return 1
1240
1241 result = 0
1242 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001243 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001244 if net.url_read_json(url, data={}, method='POST') is None:
1245 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001246 result = 1
1247 return result
1248
1249
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001250def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001251 """Returns information about the bots connected to the Swarming server."""
1252 add_filter_options(parser)
1253 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001254 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001255 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001256 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001257 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001258 help='Keep both dead and alive bots')
1259 parser.filter_group.add_option(
1260 '--busy', action='store_true', help='Keep only busy bots')
1261 parser.filter_group.add_option(
1262 '--idle', action='store_true', help='Keep only idle bots')
1263 parser.filter_group.add_option(
1264 '--mp', action='store_true',
1265 help='Keep only Machine Provider managed bots')
1266 parser.filter_group.add_option(
1267 '--non-mp', action='store_true',
1268 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001269 parser.filter_group.add_option(
1270 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001271 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001272 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001273 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001274
1275 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001276 parser.error('Use only one of --keep-dead or --dead-only')
1277 if options.busy and options.idle:
1278 parser.error('Use only one of --busy or --idle')
1279 if options.mp and options.non_mp:
1280 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001281
maruelaf6b06c2017-06-08 06:26:53 -07001282 url = options.swarming + '/api/swarming/v1/bots/list?'
1283 values = []
1284 if options.dead_only:
1285 values.append(('is_dead', 'TRUE'))
1286 elif options.keep_dead:
1287 values.append(('is_dead', 'NONE'))
1288 else:
1289 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001290
maruelaf6b06c2017-06-08 06:26:53 -07001291 if options.busy:
1292 values.append(('is_busy', 'TRUE'))
1293 elif options.idle:
1294 values.append(('is_busy', 'FALSE'))
1295 else:
1296 values.append(('is_busy', 'NONE'))
1297
1298 if options.mp:
1299 values.append(('is_mp', 'TRUE'))
1300 elif options.non_mp:
1301 values.append(('is_mp', 'FALSE'))
1302 else:
1303 values.append(('is_mp', 'NONE'))
1304
1305 for key, value in options.dimensions:
1306 values.append(('dimensions', '%s:%s' % (key, value)))
1307 url += urllib.urlencode(values)
1308 try:
1309 data, yielder = get_yielder(url, 0)
1310 bots = data.get('items') or []
1311 for items in yielder():
1312 if items:
1313 bots.extend(items)
1314 except Failure as e:
1315 sys.stderr.write('\n%s\n' % e)
1316 return 1
maruel77f720b2015-09-15 12:35:22 -07001317 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001318 print bot['bot_id']
1319 if not options.bare:
1320 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1321 print ' %s' % json.dumps(dimensions, sort_keys=True)
1322 if bot.get('task_id'):
1323 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001324 return 0
1325
1326
maruelfd0a90c2016-06-10 11:51:10 -07001327@subcommand.usage('task_id')
1328def CMDcancel(parser, args):
1329 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001330 parser.add_option(
1331 '-k', '--kill-running', action='store_true', default=False,
1332 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001333 options, args = parser.parse_args(args)
1334 if not args:
1335 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001336 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001337 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001338 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001339 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001340 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001341 print('Deleting %s failed. Probably already gone' % task_id)
1342 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001343 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001344 return 0
1345
1346
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001347@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001348def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001349 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001350
1351 The result can be in multiple part if the execution was sharded. It can
1352 potentially have retries.
1353 """
1354 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001355 parser.add_option(
1356 '-j', '--json',
1357 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001358 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001359 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001360 if not args and not options.json:
1361 parser.error('Must specify at least one task id or --json.')
1362 if args and options.json:
1363 parser.error('Only use one of task id or --json.')
1364
1365 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001366 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001367 try:
maruel1ceb3872015-10-14 06:10:44 -07001368 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001369 data = json.load(f)
1370 except (IOError, ValueError):
1371 parser.error('Failed to open %s' % options.json)
1372 try:
1373 tasks = sorted(
1374 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1375 args = [t['task_id'] for t in tasks]
1376 except (KeyError, TypeError):
1377 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001378 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001379 # Take in account all the task slices.
1380 offset = 0
1381 for s in data['request']['task_slices']:
1382 m = (offset + s['properties']['execution_timeout_secs'] +
1383 s['expiration_secs'])
1384 if m > options.timeout:
1385 options.timeout = m
1386 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001387 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001388 else:
1389 valid = frozenset('0123456789abcdef')
1390 if any(not valid.issuperset(task_id) for task_id in args):
1391 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001392
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001393 try:
1394 return collect(
1395 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001396 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001397 options.timeout,
1398 options.decorate,
1399 options.print_status_updates,
1400 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001401 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001402 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001403 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001404 except Failure:
1405 on_error.report(None)
1406 return 1
1407
1408
maruel77f720b2015-09-15 12:35:22 -07001409@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001410def CMDpost(parser, args):
1411 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1412
1413 Input data must be sent to stdin, result is printed to stdout.
1414
1415 If HTTP response code >= 400, returns non-zero.
1416 """
1417 options, args = parser.parse_args(args)
1418 if len(args) != 1:
1419 parser.error('Must specify only API name')
1420 url = options.swarming + '/api/swarming/v1/' + args[0]
1421 data = sys.stdin.read()
1422 try:
1423 resp = net.url_read(url, data=data, method='POST')
1424 except net.TimeoutError:
1425 sys.stderr.write('Timeout!\n')
1426 return 1
1427 if not resp:
1428 sys.stderr.write('No response!\n')
1429 return 1
1430 sys.stdout.write(resp)
1431 return 0
1432
1433
1434@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001435def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001436 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1437 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001438
1439 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001440 Raw task request and results:
1441 swarming.py query -S server-url.com task/123456/request
1442 swarming.py query -S server-url.com task/123456/result
1443
maruel77f720b2015-09-15 12:35:22 -07001444 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001445 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001446
maruelaf6b06c2017-06-08 06:26:53 -07001447 Listing last 10 tasks on a specific bot named 'bot1':
1448 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001449
maruelaf6b06c2017-06-08 06:26:53 -07001450 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001451 quoting is important!:
1452 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001453 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001454 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001455 parser.add_option(
1456 '-L', '--limit', type='int', default=200,
1457 help='Limit to enforce on limitless items (like number of tasks); '
1458 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001459 parser.add_option(
1460 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001461 parser.add_option(
1462 '--progress', action='store_true',
1463 help='Prints a dot at each request to show progress')
1464 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001465 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001466 parser.error(
1467 'Must specify only method name and optionally query args properly '
1468 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001469 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001470 try:
1471 data, yielder = get_yielder(base_url, options.limit)
1472 for items in yielder():
1473 if items:
1474 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001475 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001476 sys.stderr.write('.')
1477 sys.stderr.flush()
1478 except Failure as e:
1479 sys.stderr.write('\n%s\n' % e)
1480 return 1
maruel77f720b2015-09-15 12:35:22 -07001481 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001482 sys.stderr.write('\n')
1483 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001484 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001485 options.json = unicode(os.path.abspath(options.json))
1486 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001487 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001488 try:
maruel77f720b2015-09-15 12:35:22 -07001489 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001490 sys.stdout.write('\n')
1491 except IOError:
1492 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001493 return 0
1494
1495
maruel77f720b2015-09-15 12:35:22 -07001496def CMDquery_list(parser, args):
1497 """Returns list of all the Swarming APIs that can be used with command
1498 'query'.
1499 """
1500 parser.add_option(
1501 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1502 options, args = parser.parse_args(args)
1503 if args:
1504 parser.error('No argument allowed.')
1505
1506 try:
1507 apis = endpoints_api_discovery_apis(options.swarming)
1508 except APIError as e:
1509 parser.error(str(e))
1510 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001511 options.json = unicode(os.path.abspath(options.json))
1512 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001513 json.dump(apis, f)
1514 else:
1515 help_url = (
1516 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1517 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001518 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1519 if i:
1520 print('')
maruel77f720b2015-09-15 12:35:22 -07001521 print api_id
maruel11e31af2017-02-15 07:30:50 -08001522 print ' ' + api['description'].strip()
1523 if 'resources' in api:
1524 # Old.
1525 for j, (resource_name, resource) in enumerate(
1526 sorted(api['resources'].iteritems())):
1527 if j:
1528 print('')
1529 for method_name, method in sorted(resource['methods'].iteritems()):
1530 # Only list the GET ones.
1531 if method['httpMethod'] != 'GET':
1532 continue
1533 print '- %s.%s: %s' % (
1534 resource_name, method_name, method['path'])
1535 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001536 ' ' + l for l in textwrap.wrap(
1537 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001538 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1539 else:
1540 # New.
1541 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001542 # Only list the GET ones.
1543 if method['httpMethod'] != 'GET':
1544 continue
maruel11e31af2017-02-15 07:30:50 -08001545 print '- %s: %s' % (method['id'], method['path'])
1546 print('\n'.join(
1547 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001548 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1549 return 0
1550
1551
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001552@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001553def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001554 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001555
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001556 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001557 """
1558 add_trigger_options(parser)
1559 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001560 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001561 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001562 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001563 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001564 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001565 tasks = trigger_task_shards(
1566 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001567 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001568 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001569 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001570 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001571 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001572 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001573 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001574 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001575 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001576 task_ids = [
1577 t['task_id']
1578 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1579 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001580 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001581 offset = 0
1582 for s in task_request.task_slices:
1583 m = (offset + s['properties'].execution_timeout_secs +
1584 s['expiration_secs'])
1585 if m > options.timeout:
1586 options.timeout = m
1587 offset += s['expiration_secs']
1588 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001589 try:
1590 return collect(
1591 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001592 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001593 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001594 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001595 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001596 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001597 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001598 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001599 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001600 except Failure:
1601 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001602 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001603
1604
maruel18122c62015-10-23 06:31:23 -07001605@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001606def CMDreproduce(parser, args):
1607 """Runs a task locally that was triggered on the server.
1608
1609 This running locally the same commands that have been run on the bot. The data
1610 downloaded will be in a subdirectory named 'work' of the current working
1611 directory.
maruel18122c62015-10-23 06:31:23 -07001612
1613 You can pass further additional arguments to the target command by passing
1614 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001615 """
maruelc070e672016-02-22 17:32:57 -08001616 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001617 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001618 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001619 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001620 extra_args = []
1621 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001622 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001623 if len(args) > 1:
1624 if args[1] == '--':
1625 if len(args) > 2:
1626 extra_args = args[2:]
1627 else:
1628 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001629
maruel380e3262016-08-31 16:10:06 -07001630 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001631 request = net.url_read_json(url)
1632 if not request:
1633 print >> sys.stderr, 'Failed to retrieve request data for the task'
1634 return 1
1635
maruel12e30012015-10-09 11:55:35 -07001636 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001637 if fs.isdir(workdir):
1638 parser.error('Please delete the directory \'work\' first')
1639 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001640 cachedir = unicode(os.path.abspath('cipd_cache'))
1641 if not fs.exists(cachedir):
1642 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001643
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001644 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001645 env = os.environ.copy()
1646 env['SWARMING_BOT_ID'] = 'reproduce'
1647 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001648 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001649 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001650 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001651 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001652 if not i['value']:
1653 env.pop(key, None)
1654 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001655 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001656
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001657 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001658 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001659 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001660 for i in env_prefixes:
1661 key = i['key']
1662 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001663 cur = env.get(key)
1664 if cur:
1665 paths.append(cur)
1666 env[key] = os.path.pathsep.join(paths)
1667
iannucci31ab9192017-05-02 19:11:56 -07001668 command = []
nodir152cba62016-05-12 16:08:56 -07001669 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001670 # Create the tree.
1671 with isolateserver.get_storage(
1672 properties['inputs_ref']['isolatedserver'],
1673 properties['inputs_ref']['namespace']) as storage:
1674 bundle = isolateserver.fetch_isolated(
1675 properties['inputs_ref']['isolated'],
1676 storage,
1677 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001678 workdir,
1679 False)
maruel29ab2fd2015-10-16 11:44:01 -07001680 command = bundle.command
1681 if bundle.relative_cwd:
1682 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001683 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001684
1685 if properties.get('command'):
1686 command.extend(properties['command'])
1687
1688 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001689 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001690 if not options.output_dir:
1691 new_command = run_isolated.process_command(command, 'invalid', None)
1692 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001693 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001694 else:
1695 # Make the path absolute, as the process will run from a subdirectory.
1696 options.output_dir = os.path.abspath(options.output_dir)
1697 new_command = run_isolated.process_command(
1698 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001699 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001700 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001701 command = new_command
1702 file_path.ensure_command_has_abs_path(command, workdir)
1703
1704 if properties.get('cipd_input'):
1705 ci = properties['cipd_input']
1706 cp = ci['client_package']
1707 client_manager = cipd.get_client(
1708 ci['server'], cp['package_name'], cp['version'], cachedir)
1709
1710 with client_manager as client:
1711 by_path = collections.defaultdict(list)
1712 for pkg in ci['packages']:
1713 path = pkg['path']
1714 # cipd deals with 'root' as ''
1715 if path == '.':
1716 path = ''
1717 by_path[path].append((pkg['package_name'], pkg['version']))
1718 client.ensure(workdir, by_path, cache_dir=cachedir)
1719
maruel77f720b2015-09-15 12:35:22 -07001720 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001721 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001722 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001723 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001724 print >> sys.stderr, str(e)
1725 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001726
1727
maruel0eb1d1b2015-10-02 14:48:21 -07001728@subcommand.usage('bot_id')
1729def CMDterminate(parser, args):
1730 """Tells a bot to gracefully shut itself down as soon as it can.
1731
1732 This is done by completing whatever current task there is then exiting the bot
1733 process.
1734 """
1735 parser.add_option(
1736 '--wait', action='store_true', help='Wait for the bot to terminate')
1737 options, args = parser.parse_args(args)
1738 if len(args) != 1:
1739 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001740 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001741 request = net.url_read_json(url, data={})
1742 if not request:
1743 print >> sys.stderr, 'Failed to ask for termination'
1744 return 1
1745 if options.wait:
1746 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001747 options.swarming,
1748 [request['task_id']],
1749 0.,
1750 False,
1751 False,
1752 None,
1753 None,
1754 [],
maruel9531ce02016-04-13 06:11:23 -07001755 False)
maruelbfc5f872017-06-10 16:43:17 -07001756 else:
1757 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001758 return 0
1759
1760
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001761@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001762def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001763 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001764
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001765 Passes all extra arguments provided after '--' as additional command line
1766 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001767 """
1768 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001769 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001770 parser.add_option(
1771 '--dump-json',
1772 metavar='FILE',
1773 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001774 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001775 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001776 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001777 tasks = trigger_task_shards(
1778 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001779 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001780 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001781 tasks_sorted = sorted(
1782 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001783 if options.dump_json:
1784 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001785 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001786 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001787 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001788 }
maruel46b015f2015-10-13 18:40:35 -07001789 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001790 print('To collect results, use:')
1791 print(' swarming.py collect -S %s --json %s' %
1792 (options.swarming, options.dump_json))
1793 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001794 print('To collect results, use:')
1795 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001796 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1797 print('Or visit:')
1798 for t in tasks_sorted:
1799 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001800 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001801 except Failure:
1802 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001803 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001804
1805
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001806class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001807 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001808 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001809 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001810 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001811 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001812 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001813 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001814 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001815 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001816 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001817
1818 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001819 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001820 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001821 auth.process_auth_options(self, options)
1822 user = self._process_swarming(options)
1823 if hasattr(options, 'user') and not options.user:
1824 options.user = user
1825 return options, args
1826
1827 def _process_swarming(self, options):
1828 """Processes the --swarming option and aborts if not specified.
1829
1830 Returns the identity as determined by the server.
1831 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001832 if not options.swarming:
1833 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001834 try:
1835 options.swarming = net.fix_url(options.swarming)
1836 except ValueError as e:
1837 self.error('--swarming %s' % e)
1838 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001839 try:
1840 user = auth.ensure_logged_in(options.swarming)
1841 except ValueError as e:
1842 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001843 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001844
1845
1846def main(args):
1847 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001848 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001849
1850
1851if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001852 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001853 fix_encoding.fix_encoding()
1854 tools.disable_buffering()
1855 colorama.init()
1856 sys.exit(main(sys.argv[1:]))