blob: 7aedd1214c7df58be1c599a8c777e1e201bf9d19 [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 Ruel2e52c552018-03-26 19:27:36 -04008__version__ = '0.11'
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 Ruel12a7da42014-10-01 08:29:47 -0400294
maruel77f720b2015-09-15 12:35:22 -0700295 STATES = (
296 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400297 'COMPLETED', 'KILLED')
maruel77f720b2015-09-15 12:35:22 -0700298 STATES_RUNNING = ('RUNNING', 'PENDING')
299 STATES_NOT_RUNNING = (
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400300 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED', 'KILLED')
301 STATES_DONE = ('TIMED_OUT', 'COMPLETED', 'KILLED')
maruel77f720b2015-09-15 12:35:22 -0700302 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400303
304 _NAMES = {
305 RUNNING: 'Running',
306 PENDING: 'Pending',
307 EXPIRED: 'Expired',
308 TIMED_OUT: 'Execution timed out',
309 BOT_DIED: 'Bot died',
310 CANCELED: 'User canceled',
311 COMPLETED: 'Completed',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400312 KILLED: 'User killed',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400313 }
314
maruel77f720b2015-09-15 12:35:22 -0700315 _ENUMS = {
316 'RUNNING': RUNNING,
317 'PENDING': PENDING,
318 'EXPIRED': EXPIRED,
319 'TIMED_OUT': TIMED_OUT,
320 'BOT_DIED': BOT_DIED,
321 'CANCELED': CANCELED,
322 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400323 'KILLED': KILLED,
maruel77f720b2015-09-15 12:35:22 -0700324 }
325
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400326 @classmethod
327 def to_string(cls, state):
328 """Returns a user-readable string representing a State."""
329 if state not in cls._NAMES:
330 raise ValueError('Invalid state %s' % state)
331 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000332
maruel77f720b2015-09-15 12:35:22 -0700333 @classmethod
334 def from_enum(cls, state):
335 """Returns int value based on the string."""
336 if state not in cls._ENUMS:
337 raise ValueError('Invalid state %s' % state)
338 return cls._ENUMS[state]
339
maruel@chromium.org0437a732013-08-27 16:05:52 +0000340
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700341class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700342 """Assembles task execution summary (for --task-summary-json output).
343
344 Optionally fetches task outputs from isolate server to local disk (used when
345 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700346
347 This object is shared among multiple threads running 'retrieve_results'
348 function, in particular they call 'process_shard_result' method in parallel.
349 """
350
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000351 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700352 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
353
354 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700355 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700356 shard_count: expected number of task shards.
357 """
maruel12e30012015-10-09 11:55:35 -0700358 self.task_output_dir = (
359 unicode(os.path.abspath(task_output_dir))
360 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000361 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362 self.shard_count = shard_count
363
364 self._lock = threading.Lock()
365 self._per_shard_results = {}
366 self._storage = None
367
nodire5028a92016-04-29 14:38:21 -0700368 if self.task_output_dir:
369 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370
Vadim Shtayurab450c602014-05-12 19:23:25 -0700371 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 """Stores results of a single task shard, fetches output files if necessary.
373
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400374 Modifies |result| in place.
375
maruel77f720b2015-09-15 12:35:22 -0700376 shard_index is 0-based.
377
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700378 Called concurrently from multiple threads.
379 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700380 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700381 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 if shard_index < 0 or shard_index >= self.shard_count:
383 logging.warning(
384 'Shard index %d is outside of expected range: [0; %d]',
385 shard_index, self.shard_count - 1)
386 return
387
maruel77f720b2015-09-15 12:35:22 -0700388 if result.get('outputs_ref'):
389 ref = result['outputs_ref']
390 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
391 ref['isolatedserver'],
392 urllib.urlencode(
393 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400394
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700395 # Store result dict of that shard, ignore results we've already seen.
396 with self._lock:
397 if shard_index in self._per_shard_results:
398 logging.warning('Ignoring duplicate shard index %d', shard_index)
399 return
400 self._per_shard_results[shard_index] = result
401
402 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700403 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400404 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700405 result['outputs_ref']['isolatedserver'],
406 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400407 if storage:
408 # Output files are supposed to be small and they are not reused across
409 # tasks. So use MemoryCache for them instead of on-disk cache. Make
410 # files writable, so that calling script can delete them.
411 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700412 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400413 storage,
414 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700415 os.path.join(self.task_output_dir, str(shard_index)),
416 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700417
418 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700419 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700420 with self._lock:
421 # Write an array of shard results with None for missing shards.
422 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700423 'shards': [
424 self._per_shard_results.get(i) for i in xrange(self.shard_count)
425 ],
426 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000427
428 # Don't store stdout in the summary if not requested too.
429 if "json" not in self.task_output_stdout:
430 for shard_json in summary['shards']:
431 if not shard_json:
432 continue
433 if "output" in shard_json:
434 del shard_json["output"]
435 if "outputs" in shard_json:
436 del shard_json["outputs"]
437
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700438 # Write summary.json to task_output_dir as well.
439 if self.task_output_dir:
440 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700441 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 summary,
443 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700444 if self._storage:
445 self._storage.close()
446 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700447 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448
449 def _get_storage(self, isolate_server, namespace):
450 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700451 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452 with self._lock:
453 if not self._storage:
454 self._storage = isolateserver.get_storage(isolate_server, namespace)
455 else:
456 # Shards must all use exact same isolate server and namespace.
457 if self._storage.location != isolate_server:
458 logging.error(
459 'Task shards are using multiple isolate servers: %s and %s',
460 self._storage.location, isolate_server)
461 return None
462 if self._storage.namespace != namespace:
463 logging.error(
464 'Task shards are using multiple namespaces: %s and %s',
465 self._storage.namespace, namespace)
466 return None
467 return self._storage
468
469
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500470def now():
471 """Exists so it can be mocked easily."""
472 return time.time()
473
474
maruel77f720b2015-09-15 12:35:22 -0700475def parse_time(value):
476 """Converts serialized time from the API to datetime.datetime."""
477 # When microseconds are 0, the '.123456' suffix is elided. This means the
478 # serialized format is not consistent, which confuses the hell out of python.
479 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
480 try:
481 return datetime.datetime.strptime(value, fmt)
482 except ValueError:
483 pass
484 raise ValueError('Failed to parse %s' % value)
485
486
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700487def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700488 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000489 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400490 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700491
Vadim Shtayurab450c602014-05-12 19:23:25 -0700492 Returns:
493 <result dict> on success.
494 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700495 """
maruel71c61c82016-02-22 06:52:05 -0800496 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700497 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700498 if include_perf:
499 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700500 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700501 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400502 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700503 attempt = 0
504
505 while not should_stop.is_set():
506 attempt += 1
507
508 # Waiting for too long -> give up.
509 current_time = now()
510 if deadline and current_time >= deadline:
511 logging.error('retrieve_results(%s) timed out on attempt %d',
512 base_url, attempt)
513 return None
514
515 # Do not spin too fast. Spin faster at the beginning though.
516 # Start with 1 sec delay and for each 30 sec of waiting add another second
517 # of delay, until hitting 15 sec ceiling.
518 if attempt > 1:
519 max_delay = min(15, 1 + (current_time - started) / 30.0)
520 delay = min(max_delay, deadline - current_time) if deadline else max_delay
521 if delay > 0:
522 logging.debug('Waiting %.1f sec before retrying', delay)
523 should_stop.wait(delay)
524 if should_stop.is_set():
525 return None
526
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400527 # Disable internal retries in net.url_read_json, since we are doing retries
528 # ourselves.
529 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700530 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
531 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400532 # Retry on 500s only if no timeout is specified.
533 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400534 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400535 if timeout == -1:
536 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400537 continue
maruel77f720b2015-09-15 12:35:22 -0700538
maruelbf53e042015-12-01 15:00:51 -0800539 if result.get('error'):
540 # An error occurred.
541 if result['error'].get('errors'):
542 for err in result['error']['errors']:
543 logging.warning(
544 'Error while reading task: %s; %s',
545 err.get('message'), err.get('debugInfo'))
546 elif result['error'].get('message'):
547 logging.warning(
548 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400549 if timeout == -1:
550 return result
maruelbf53e042015-12-01 15:00:51 -0800551 continue
552
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400553 # When timeout == -1, always return on first attempt. 500s are already
554 # retried in this case.
555 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000556 if fetch_stdout:
557 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700558 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700559 # Record the result, try to fetch attached output files (if any).
560 if output_collector:
561 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700562 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700563 if result.get('internal_failure'):
564 logging.error('Internal error!')
565 elif result['state'] == 'BOT_DIED':
566 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700567 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000568
569
maruel77f720b2015-09-15 12:35:22 -0700570def convert_to_old_format(result):
571 """Converts the task result data from Endpoints API format to old API format
572 for compatibility.
573
574 This goes into the file generated as --task-summary-json.
575 """
576 # Sets default.
577 result.setdefault('abandoned_ts', None)
578 result.setdefault('bot_id', None)
579 result.setdefault('bot_version', None)
580 result.setdefault('children_task_ids', [])
581 result.setdefault('completed_ts', None)
582 result.setdefault('cost_saved_usd', None)
583 result.setdefault('costs_usd', None)
584 result.setdefault('deduped_from', None)
585 result.setdefault('name', None)
586 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700587 result.setdefault('server_versions', None)
588 result.setdefault('started_ts', None)
589 result.setdefault('tags', None)
590 result.setdefault('user', None)
591
592 # Convertion back to old API.
593 duration = result.pop('duration', None)
594 result['durations'] = [duration] if duration else []
595 exit_code = result.pop('exit_code', None)
596 result['exit_codes'] = [int(exit_code)] if exit_code else []
597 result['id'] = result.pop('task_id')
598 result['isolated_out'] = result.get('outputs_ref', None)
599 output = result.pop('output', None)
600 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700601 # server_version
602 # Endpoints result 'state' as string. For compatibility with old code, convert
603 # to int.
604 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700605 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700606 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700607 if 'bot_dimensions' in result:
608 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700609 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700610 }
611 else:
612 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700613
614
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700615def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400616 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000617 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500618 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000619
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700620 Duplicate shards are ignored. Shards are yielded in order of completion.
621 Timed out shards are NOT yielded at all. Caller can compare number of yielded
622 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000623
624 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500625 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 +0000626 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500627
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700628 output_collector is an optional instance of TaskOutputCollector that will be
629 used to fetch files produced by a task from isolate server to the local disk.
630
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500631 Yields:
632 (index, result). In particular, 'result' is defined as the
633 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000634 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000635 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400636 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700637 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700638 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700639
maruel@chromium.org0437a732013-08-27 16:05:52 +0000640 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
641 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700642 # Adds a task to the thread pool to call 'retrieve_results' and return
643 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400644 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700645 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000646 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400647 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000648 task_id, timeout, should_stop, output_collector, include_perf,
649 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700650
651 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400652 for shard_index, task_id in enumerate(task_ids):
653 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700654
655 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400656 shards_remaining = range(len(task_ids))
657 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700658 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700659 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700660 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700661 shard_index, result = results_channel.pull(
662 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700663 except threading_utils.TaskChannel.Timeout:
664 if print_status_updates:
665 print(
666 'Waiting for results from the following shards: %s' %
667 ', '.join(map(str, shards_remaining)))
668 sys.stdout.flush()
669 continue
670 except Exception:
671 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700672
673 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700674 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000675 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500676 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000677 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700678
Vadim Shtayurab450c602014-05-12 19:23:25 -0700679 # Yield back results to the caller.
680 assert shard_index in shards_remaining
681 shards_remaining.remove(shard_index)
682 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700683
maruel@chromium.org0437a732013-08-27 16:05:52 +0000684 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700685 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000686 should_stop.set()
687
688
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000689def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700691 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400692 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700693 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
694 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400695 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
696 metadata.get('abandoned_ts')):
697 pending = '%.1fs' % (
698 parse_time(metadata['abandoned_ts']) -
699 parse_time(metadata['created_ts'])
700 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400701 else:
702 pending = 'N/A'
703
maruel77f720b2015-09-15 12:35:22 -0700704 if metadata.get('duration') is not None:
705 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400706 else:
707 duration = 'N/A'
708
maruel77f720b2015-09-15 12:35:22 -0700709 if metadata.get('exit_code') is not None:
710 # Integers are encoded as string to not loose precision.
711 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400712 else:
713 exit_code = 'N/A'
714
715 bot_id = metadata.get('bot_id') or 'N/A'
716
maruel77f720b2015-09-15 12:35:22 -0700717 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400718 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000719 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400720 if metadata.get('state') == 'CANCELED':
721 tag_footer2 = ' Pending: %s CANCELED' % pending
722 elif metadata.get('state') == 'EXPIRED':
723 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400724 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400725 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
726 pending, duration, bot_id, exit_code, metadata['state'])
727 else:
728 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
729 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400730
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000731 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
732 dash_pad = '+-%s-+' % ('-' * tag_len)
733 tag_header = '| %s |' % tag_header.ljust(tag_len)
734 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
735 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400736
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000737 if include_stdout:
738 return '\n'.join([
739 dash_pad,
740 tag_header,
741 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400742 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000743 dash_pad,
744 tag_footer1,
745 tag_footer2,
746 dash_pad,
747 ])
748 else:
749 return '\n'.join([
750 dash_pad,
751 tag_header,
752 tag_footer2,
753 dash_pad,
754 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000755
756
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700757def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700758 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000759 task_summary_json, task_output_dir, task_output_stdout,
760 include_perf):
maruela5490782015-09-30 10:56:59 -0700761 """Retrieves results of a Swarming task.
762
763 Returns:
764 process exit code that should be returned to the user.
765 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700766 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000767 output_collector = TaskOutputCollector(
768 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700770 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700771 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400772 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400774 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400775 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000776 output_collector, include_perf,
777 (len(task_output_stdout) > 0),
778 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700779 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700780
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400781 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700782 shard_exit_code = metadata.get('exit_code')
783 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700784 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700785 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700786 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400787 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700788 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700789
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700790 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000791 s = decorate_shard_output(
792 swarming, index, metadata,
793 "console" in task_output_stdout).encode(
794 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700795 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400796 if len(seen_shards) < len(task_ids):
797 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700798 else:
maruel77f720b2015-09-15 12:35:22 -0700799 print('%s: %s %s' % (
800 metadata.get('bot_id', 'N/A'),
801 metadata['task_id'],
802 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000803 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700804 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400805 if output:
806 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700807 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700808 summary = output_collector.finalize()
809 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700810 # TODO(maruel): Make this optional.
811 for i in summary['shards']:
812 if i:
813 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700814 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700815
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400816 if decorate and total_duration:
817 print('Total duration: %.1fs' % total_duration)
818
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400819 if len(seen_shards) != len(task_ids):
820 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700821 print >> sys.stderr, ('Results from some shards are missing: %s' %
822 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700823 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700824
maruela5490782015-09-30 10:56:59 -0700825 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000826
827
maruel77f720b2015-09-15 12:35:22 -0700828### API management.
829
830
831class APIError(Exception):
832 pass
833
834
835def endpoints_api_discovery_apis(host):
836 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
837 the APIs exposed by a host.
838
839 https://developers.google.com/discovery/v1/reference/apis/list
840 """
maruel380e3262016-08-31 16:10:06 -0700841 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
842 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700843 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
844 if data is None:
845 raise APIError('Failed to discover APIs on %s' % host)
846 out = {}
847 for api in data['items']:
848 if api['id'] == 'discovery:v1':
849 continue
850 # URL is of the following form:
851 # url = host + (
852 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
853 api_data = net.url_read_json(api['discoveryRestUrl'])
854 if api_data is None:
855 raise APIError('Failed to discover %s on %s' % (api['id'], host))
856 out[api['id']] = api_data
857 return out
858
859
maruelaf6b06c2017-06-08 06:26:53 -0700860def get_yielder(base_url, limit):
861 """Returns the first query and a function that yields following items."""
862 CHUNK_SIZE = 250
863
864 url = base_url
865 if limit:
866 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
867 data = net.url_read_json(url)
868 if data is None:
869 # TODO(maruel): Do basic diagnostic.
870 raise Failure('Failed to access %s' % url)
871 org_cursor = data.pop('cursor', None)
872 org_total = len(data.get('items') or [])
873 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
874 if not org_cursor or not org_total:
875 # This is not an iterable resource.
876 return data, lambda: []
877
878 def yielder():
879 cursor = org_cursor
880 total = org_total
881 # Some items support cursors. Try to get automatically if cursors are needed
882 # by looking at the 'cursor' items.
883 while cursor and (not limit or total < limit):
884 merge_char = '&' if '?' in base_url else '?'
885 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
886 if limit:
887 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
888 new = net.url_read_json(url)
889 if new is None:
890 raise Failure('Failed to access %s' % url)
891 cursor = new.get('cursor')
892 new_items = new.get('items')
893 nb_items = len(new_items or [])
894 total += nb_items
895 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
896 yield new_items
897
898 return data, yielder
899
900
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500901### Commands.
902
903
904def abort_task(_swarming, _manifest):
905 """Given a task manifest that was triggered, aborts its execution."""
906 # TODO(vadimsh): No supported by the server yet.
907
908
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400909def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800910 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500911 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500912 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500913 dest='dimensions', metavar='FOO bar',
914 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500915 parser.add_option_group(parser.filter_group)
916
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400917
maruelaf6b06c2017-06-08 06:26:53 -0700918def process_filter_options(parser, options):
919 for key, value in options.dimensions:
920 if ':' in key:
921 parser.error('--dimension key cannot contain ":"')
922 if key.strip() != key:
923 parser.error('--dimension key has whitespace')
924 if not key:
925 parser.error('--dimension key is empty')
926
927 if value.strip() != value:
928 parser.error('--dimension value has whitespace')
929 if not value:
930 parser.error('--dimension value is empty')
931 options.dimensions.sort()
932
933
Vadim Shtayurab450c602014-05-12 19:23:25 -0700934def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400935 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700936 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700937 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700938 help='Number of shards to trigger and collect.')
939 parser.add_option_group(parser.sharding_group)
940
941
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400942def add_trigger_options(parser):
943 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500944 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400945 add_filter_options(parser)
946
maruel681d6802017-01-17 16:56:03 -0800947 group = optparse.OptionGroup(parser, 'Task properties')
948 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700949 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500950 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800951 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500952 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700953 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800954 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800955 '--env-prefix', default=[], action='append', nargs=2,
956 metavar='VAR local/path',
957 help='Prepend task-relative `local/path` to the task\'s VAR environment '
958 'variable using os-appropriate pathsep character. Can be specified '
959 'multiple times for the same VAR to add multiple paths.')
960 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400961 '--idempotent', action='store_true', default=False,
962 help='When set, the server will actively try to find a previous task '
963 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800964 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700965 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700966 help='The optional path to a file containing the secret_bytes to use with'
967 'this task.')
maruel681d6802017-01-17 16:56:03 -0800968 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700969 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400970 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700972 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400973 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800974 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500975 '--raw-cmd', action='store_true', default=False,
976 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700977 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800978 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500979 '--relative-cwd',
980 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
981 'requires --raw-cmd')
982 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700983 '--cipd-package', action='append', default=[], metavar='PKG',
984 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700985 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800986 group.add_option(
987 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700988 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800989 help='"<name> <relpath>" items to keep a persistent bot managed cache')
990 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700991 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700992 help='Email of a service account to run the task as, or literal "bot" '
993 'string to indicate that the task should use the same account the '
994 'bot itself is using to authenticate to Swarming. Don\'t use task '
995 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800996 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700997 '-o', '--output', action='append', default=[], metavar='PATH',
998 help='A list of files to return in addition to those written to '
999 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1000 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -08001001 parser.add_option_group(group)
1002
1003 group = optparse.OptionGroup(parser, 'Task request')
1004 group.add_option(
1005 '--priority', type='int', default=100,
1006 help='The lower value, the more important the task is')
1007 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001008 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001009 help='Display name of the task. Defaults to '
1010 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1011 'isolated file is provided, if a hash is provided, it defaults to '
1012 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1013 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001014 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001015 help='Tags to assign to the task.')
1016 group.add_option(
1017 '--user', default='',
1018 help='User associated with the task. Defaults to authenticated user on '
1019 'the server.')
1020 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001021 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001022 help='Seconds to allow the task to be pending for a bot to run before '
1023 'this task request expires.')
1024 group.add_option(
1025 '--deadline', type='int', dest='expiration',
1026 help=optparse.SUPPRESS_HELP)
1027 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001028
1029
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001030def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001031 """Processes trigger options and does preparatory steps.
1032
1033 Returns:
1034 NewTaskRequest instance.
1035 """
maruelaf6b06c2017-06-08 06:26:53 -07001036 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001037 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001038 if args and args[0] == '--':
1039 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001040
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001041 if not options.dimensions:
1042 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001043 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1044 parser.error('--tags must be in the format key:value')
1045 if options.raw_cmd and not args:
1046 parser.error(
1047 'Arguments with --raw-cmd should be passed after -- as command '
1048 'delimiter.')
1049 if options.isolate_server and not options.namespace:
1050 parser.error(
1051 '--namespace must be a valid value when --isolate-server is used')
1052 if not options.isolated and not options.raw_cmd:
1053 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1054
1055 # Isolated
1056 # --isolated is required only if --raw-cmd wasn't provided.
1057 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1058 # preferred server.
1059 isolateserver.process_isolate_server_options(
1060 parser, options, False, not options.raw_cmd)
1061 inputs_ref = None
1062 if options.isolate_server:
1063 inputs_ref = FilesRef(
1064 isolated=options.isolated,
1065 isolatedserver=options.isolate_server,
1066 namespace=options.namespace)
1067
1068 # Command
1069 command = None
1070 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001071 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001072 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001073 if options.relative_cwd:
1074 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1075 if not a.startswith(os.getcwd()):
1076 parser.error(
1077 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001078 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001079 if options.relative_cwd:
1080 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001081 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001082
maruel0a25f6c2017-05-10 10:43:23 -07001083 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001084 cipd_packages = []
1085 for p in options.cipd_package:
1086 split = p.split(':', 2)
1087 if len(split) != 3:
1088 parser.error('CIPD packages must take the form: path:package:version')
1089 cipd_packages.append(CipdPackage(
1090 package_name=split[1],
1091 path=split[0],
1092 version=split[2]))
1093 cipd_input = None
1094 if cipd_packages:
1095 cipd_input = CipdInput(
1096 client_package=None,
1097 packages=cipd_packages,
1098 server=None)
1099
maruel0a25f6c2017-05-10 10:43:23 -07001100 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001101 secret_bytes = None
1102 if options.secret_bytes_path:
1103 with open(options.secret_bytes_path, 'r') as f:
1104 secret_bytes = f.read().encode('base64')
1105
maruel0a25f6c2017-05-10 10:43:23 -07001106 # Named caches
maruel681d6802017-01-17 16:56:03 -08001107 caches = [
1108 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1109 for i in options.named_cache
1110 ]
maruel0a25f6c2017-05-10 10:43:23 -07001111
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001112 env_prefixes = {}
1113 for k, v in options.env_prefix:
1114 env_prefixes.setdefault(k, []).append(v)
1115
maruel77f720b2015-09-15 12:35:22 -07001116 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001117 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001118 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001119 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001120 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001121 dimensions=options.dimensions,
1122 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001123 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001124 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001125 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001126 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001127 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001128 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001129 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001130 outputs=options.output,
1131 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001132
maruel77f720b2015-09-15 12:35:22 -07001133 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001134 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001135 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001136 priority=options.priority,
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001137 task_slices=[
1138 {
1139 'expiration_secs': options.expiration,
1140 'properties': properties,
1141 },
1142 ],
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001143 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001144 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001145 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001146
1147
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001148class TaskOutputStdoutOption(optparse.Option):
1149 """Where to output the each task's console output (stderr/stdout).
1150
1151 The output will be;
1152 none - not be downloaded.
1153 json - stored in summary.json file *only*.
1154 console - shown on stdout *only*.
1155 all - stored in summary.json and shown on stdout.
1156 """
1157
1158 choices = ['all', 'json', 'console', 'none']
1159
1160 def __init__(self, *args, **kw):
1161 optparse.Option.__init__(
1162 self,
1163 *args,
1164 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001165 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001166 help=re.sub('\s\s*', ' ', self.__doc__),
1167 **kw)
1168
1169 def convert_value(self, opt, value):
1170 if value not in self.choices:
1171 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1172 self.get_opt_string(), self.choices, value))
1173 stdout_to = []
1174 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001175 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001176 elif value != 'none':
1177 stdout_to = [value]
1178 return stdout_to
1179
1180
maruel@chromium.org0437a732013-08-27 16:05:52 +00001181def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001182 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001183 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001184 help='Timeout to wait for result, set to -1 for no timeout and get '
1185 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001186 parser.group_logging.add_option(
1187 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001188 parser.group_logging.add_option(
1189 '--print-status-updates', action='store_true',
1190 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001191 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001192 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001193 '--task-summary-json',
1194 metavar='FILE',
1195 help='Dump a summary of task results to this file as json. It contains '
1196 'only shards statuses as know to server directly. Any output files '
1197 'emitted by the task can be collected by using --task-output-dir')
1198 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001199 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001200 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001201 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001202 'directory contains per-shard directory with output files produced '
1203 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001204 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001205 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001206 parser.task_output_group.add_option(
1207 '--perf', action='store_true', default=False,
1208 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001209 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001210
1211
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001212def process_collect_options(parser, options):
1213 # Only negative -1 is allowed, disallow other negative values.
1214 if options.timeout != -1 and options.timeout < 0:
1215 parser.error('Invalid --timeout value')
1216
1217
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001218@subcommand.usage('bots...')
1219def CMDbot_delete(parser, args):
1220 """Forcibly deletes bots from the Swarming server."""
1221 parser.add_option(
1222 '-f', '--force', action='store_true',
1223 help='Do not prompt for confirmation')
1224 options, args = parser.parse_args(args)
1225 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001226 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001227
1228 bots = sorted(args)
1229 if not options.force:
1230 print('Delete the following bots?')
1231 for bot in bots:
1232 print(' %s' % bot)
1233 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1234 print('Goodbye.')
1235 return 1
1236
1237 result = 0
1238 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001239 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001240 if net.url_read_json(url, data={}, method='POST') is None:
1241 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001242 result = 1
1243 return result
1244
1245
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001246def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001247 """Returns information about the bots connected to the Swarming server."""
1248 add_filter_options(parser)
1249 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001250 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001251 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001252 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001253 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001254 help='Keep both dead and alive bots')
1255 parser.filter_group.add_option(
1256 '--busy', action='store_true', help='Keep only busy bots')
1257 parser.filter_group.add_option(
1258 '--idle', action='store_true', help='Keep only idle bots')
1259 parser.filter_group.add_option(
1260 '--mp', action='store_true',
1261 help='Keep only Machine Provider managed bots')
1262 parser.filter_group.add_option(
1263 '--non-mp', action='store_true',
1264 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001265 parser.filter_group.add_option(
1266 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001267 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001268 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001269 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001270
1271 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001272 parser.error('Use only one of --keep-dead or --dead-only')
1273 if options.busy and options.idle:
1274 parser.error('Use only one of --busy or --idle')
1275 if options.mp and options.non_mp:
1276 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001277
maruelaf6b06c2017-06-08 06:26:53 -07001278 url = options.swarming + '/api/swarming/v1/bots/list?'
1279 values = []
1280 if options.dead_only:
1281 values.append(('is_dead', 'TRUE'))
1282 elif options.keep_dead:
1283 values.append(('is_dead', 'NONE'))
1284 else:
1285 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001286
maruelaf6b06c2017-06-08 06:26:53 -07001287 if options.busy:
1288 values.append(('is_busy', 'TRUE'))
1289 elif options.idle:
1290 values.append(('is_busy', 'FALSE'))
1291 else:
1292 values.append(('is_busy', 'NONE'))
1293
1294 if options.mp:
1295 values.append(('is_mp', 'TRUE'))
1296 elif options.non_mp:
1297 values.append(('is_mp', 'FALSE'))
1298 else:
1299 values.append(('is_mp', 'NONE'))
1300
1301 for key, value in options.dimensions:
1302 values.append(('dimensions', '%s:%s' % (key, value)))
1303 url += urllib.urlencode(values)
1304 try:
1305 data, yielder = get_yielder(url, 0)
1306 bots = data.get('items') or []
1307 for items in yielder():
1308 if items:
1309 bots.extend(items)
1310 except Failure as e:
1311 sys.stderr.write('\n%s\n' % e)
1312 return 1
maruel77f720b2015-09-15 12:35:22 -07001313 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001314 print bot['bot_id']
1315 if not options.bare:
1316 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1317 print ' %s' % json.dumps(dimensions, sort_keys=True)
1318 if bot.get('task_id'):
1319 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001320 return 0
1321
1322
maruelfd0a90c2016-06-10 11:51:10 -07001323@subcommand.usage('task_id')
1324def CMDcancel(parser, args):
1325 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001326 parser.add_option(
1327 '-k', '--kill-running', action='store_true', default=False,
1328 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001329 options, args = parser.parse_args(args)
1330 if not args:
1331 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001332 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001333 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001334 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001335 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001336 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001337 print('Deleting %s failed. Probably already gone' % task_id)
1338 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001339 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001340 return 0
1341
1342
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001343@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001344def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001345 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001346
1347 The result can be in multiple part if the execution was sharded. It can
1348 potentially have retries.
1349 """
1350 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001351 parser.add_option(
1352 '-j', '--json',
1353 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001354 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001355 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001356 if not args and not options.json:
1357 parser.error('Must specify at least one task id or --json.')
1358 if args and options.json:
1359 parser.error('Only use one of task id or --json.')
1360
1361 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001362 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001363 try:
maruel1ceb3872015-10-14 06:10:44 -07001364 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001365 data = json.load(f)
1366 except (IOError, ValueError):
1367 parser.error('Failed to open %s' % options.json)
1368 try:
1369 tasks = sorted(
1370 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1371 args = [t['task_id'] for t in tasks]
1372 except (KeyError, TypeError):
1373 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001374 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001375 # Take in account all the task slices.
1376 offset = 0
1377 for s in data['request']['task_slices']:
1378 m = (offset + s['properties']['execution_timeout_secs'] +
1379 s['expiration_secs'])
1380 if m > options.timeout:
1381 options.timeout = m
1382 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001383 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001384 else:
1385 valid = frozenset('0123456789abcdef')
1386 if any(not valid.issuperset(task_id) for task_id in args):
1387 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001388
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001389 try:
1390 return collect(
1391 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001392 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001393 options.timeout,
1394 options.decorate,
1395 options.print_status_updates,
1396 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001397 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001398 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001399 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001400 except Failure:
1401 on_error.report(None)
1402 return 1
1403
1404
maruel77f720b2015-09-15 12:35:22 -07001405@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001406def CMDpost(parser, args):
1407 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1408
1409 Input data must be sent to stdin, result is printed to stdout.
1410
1411 If HTTP response code >= 400, returns non-zero.
1412 """
1413 options, args = parser.parse_args(args)
1414 if len(args) != 1:
1415 parser.error('Must specify only API name')
1416 url = options.swarming + '/api/swarming/v1/' + args[0]
1417 data = sys.stdin.read()
1418 try:
1419 resp = net.url_read(url, data=data, method='POST')
1420 except net.TimeoutError:
1421 sys.stderr.write('Timeout!\n')
1422 return 1
1423 if not resp:
1424 sys.stderr.write('No response!\n')
1425 return 1
1426 sys.stdout.write(resp)
1427 return 0
1428
1429
1430@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001431def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001432 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1433 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001434
1435 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001436 Raw task request and results:
1437 swarming.py query -S server-url.com task/123456/request
1438 swarming.py query -S server-url.com task/123456/result
1439
maruel77f720b2015-09-15 12:35:22 -07001440 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001441 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001442
maruelaf6b06c2017-06-08 06:26:53 -07001443 Listing last 10 tasks on a specific bot named 'bot1':
1444 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001445
maruelaf6b06c2017-06-08 06:26:53 -07001446 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001447 quoting is important!:
1448 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001449 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001450 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001451 parser.add_option(
1452 '-L', '--limit', type='int', default=200,
1453 help='Limit to enforce on limitless items (like number of tasks); '
1454 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001455 parser.add_option(
1456 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001457 parser.add_option(
1458 '--progress', action='store_true',
1459 help='Prints a dot at each request to show progress')
1460 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001461 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001462 parser.error(
1463 'Must specify only method name and optionally query args properly '
1464 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001465 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001466 try:
1467 data, yielder = get_yielder(base_url, options.limit)
1468 for items in yielder():
1469 if items:
1470 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001471 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001472 sys.stderr.write('.')
1473 sys.stderr.flush()
1474 except Failure as e:
1475 sys.stderr.write('\n%s\n' % e)
1476 return 1
maruel77f720b2015-09-15 12:35:22 -07001477 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001478 sys.stderr.write('\n')
1479 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001480 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001481 options.json = unicode(os.path.abspath(options.json))
1482 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001483 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001484 try:
maruel77f720b2015-09-15 12:35:22 -07001485 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001486 sys.stdout.write('\n')
1487 except IOError:
1488 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001489 return 0
1490
1491
maruel77f720b2015-09-15 12:35:22 -07001492def CMDquery_list(parser, args):
1493 """Returns list of all the Swarming APIs that can be used with command
1494 'query'.
1495 """
1496 parser.add_option(
1497 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1498 options, args = parser.parse_args(args)
1499 if args:
1500 parser.error('No argument allowed.')
1501
1502 try:
1503 apis = endpoints_api_discovery_apis(options.swarming)
1504 except APIError as e:
1505 parser.error(str(e))
1506 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001507 options.json = unicode(os.path.abspath(options.json))
1508 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001509 json.dump(apis, f)
1510 else:
1511 help_url = (
1512 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1513 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001514 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1515 if i:
1516 print('')
maruel77f720b2015-09-15 12:35:22 -07001517 print api_id
maruel11e31af2017-02-15 07:30:50 -08001518 print ' ' + api['description'].strip()
1519 if 'resources' in api:
1520 # Old.
1521 for j, (resource_name, resource) in enumerate(
1522 sorted(api['resources'].iteritems())):
1523 if j:
1524 print('')
1525 for method_name, method in sorted(resource['methods'].iteritems()):
1526 # Only list the GET ones.
1527 if method['httpMethod'] != 'GET':
1528 continue
1529 print '- %s.%s: %s' % (
1530 resource_name, method_name, method['path'])
1531 print('\n'.join(
1532 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1533 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1534 else:
1535 # New.
1536 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001537 # Only list the GET ones.
1538 if method['httpMethod'] != 'GET':
1539 continue
maruel11e31af2017-02-15 07:30:50 -08001540 print '- %s: %s' % (method['id'], method['path'])
1541 print('\n'.join(
1542 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001543 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1544 return 0
1545
1546
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001547@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001548def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001549 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001550
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001551 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001552 """
1553 add_trigger_options(parser)
1554 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001555 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001556 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001557 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001558 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001559 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001560 tasks = trigger_task_shards(
1561 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001562 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001563 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001564 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001565 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001566 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001567 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001568 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001569 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001570 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001571 task_ids = [
1572 t['task_id']
1573 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1574 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001575 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001576 offset = 0
1577 for s in task_request.task_slices:
1578 m = (offset + s['properties'].execution_timeout_secs +
1579 s['expiration_secs'])
1580 if m > options.timeout:
1581 options.timeout = m
1582 offset += s['expiration_secs']
1583 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001584 try:
1585 return collect(
1586 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001587 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001588 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001589 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001590 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001591 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001592 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001593 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001594 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001595 except Failure:
1596 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001597 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001598
1599
maruel18122c62015-10-23 06:31:23 -07001600@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001601def CMDreproduce(parser, args):
1602 """Runs a task locally that was triggered on the server.
1603
1604 This running locally the same commands that have been run on the bot. The data
1605 downloaded will be in a subdirectory named 'work' of the current working
1606 directory.
maruel18122c62015-10-23 06:31:23 -07001607
1608 You can pass further additional arguments to the target command by passing
1609 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001610 """
maruelc070e672016-02-22 17:32:57 -08001611 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001612 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001613 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001614 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001615 extra_args = []
1616 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001617 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001618 if len(args) > 1:
1619 if args[1] == '--':
1620 if len(args) > 2:
1621 extra_args = args[2:]
1622 else:
1623 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001624
maruel380e3262016-08-31 16:10:06 -07001625 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001626 request = net.url_read_json(url)
1627 if not request:
1628 print >> sys.stderr, 'Failed to retrieve request data for the task'
1629 return 1
1630
maruel12e30012015-10-09 11:55:35 -07001631 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001632 if fs.isdir(workdir):
1633 parser.error('Please delete the directory \'work\' first')
1634 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001635 cachedir = unicode(os.path.abspath('cipd_cache'))
1636 if not fs.exists(cachedir):
1637 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001638
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001639 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001640 env = os.environ.copy()
1641 env['SWARMING_BOT_ID'] = 'reproduce'
1642 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001643 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001644 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001645 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001646 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001647 if not i['value']:
1648 env.pop(key, None)
1649 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001650 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001651
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001652 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001653 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001654 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001655 for i in env_prefixes:
1656 key = i['key']
1657 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001658 cur = env.get(key)
1659 if cur:
1660 paths.append(cur)
1661 env[key] = os.path.pathsep.join(paths)
1662
iannucci31ab9192017-05-02 19:11:56 -07001663 command = []
nodir152cba62016-05-12 16:08:56 -07001664 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001665 # Create the tree.
1666 with isolateserver.get_storage(
1667 properties['inputs_ref']['isolatedserver'],
1668 properties['inputs_ref']['namespace']) as storage:
1669 bundle = isolateserver.fetch_isolated(
1670 properties['inputs_ref']['isolated'],
1671 storage,
1672 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001673 workdir,
1674 False)
maruel29ab2fd2015-10-16 11:44:01 -07001675 command = bundle.command
1676 if bundle.relative_cwd:
1677 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001678 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001679
1680 if properties.get('command'):
1681 command.extend(properties['command'])
1682
1683 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001684 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001685 if not options.output_dir:
1686 new_command = run_isolated.process_command(command, 'invalid', None)
1687 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001688 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001689 else:
1690 # Make the path absolute, as the process will run from a subdirectory.
1691 options.output_dir = os.path.abspath(options.output_dir)
1692 new_command = run_isolated.process_command(
1693 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001694 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001695 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001696 command = new_command
1697 file_path.ensure_command_has_abs_path(command, workdir)
1698
1699 if properties.get('cipd_input'):
1700 ci = properties['cipd_input']
1701 cp = ci['client_package']
1702 client_manager = cipd.get_client(
1703 ci['server'], cp['package_name'], cp['version'], cachedir)
1704
1705 with client_manager as client:
1706 by_path = collections.defaultdict(list)
1707 for pkg in ci['packages']:
1708 path = pkg['path']
1709 # cipd deals with 'root' as ''
1710 if path == '.':
1711 path = ''
1712 by_path[path].append((pkg['package_name'], pkg['version']))
1713 client.ensure(workdir, by_path, cache_dir=cachedir)
1714
maruel77f720b2015-09-15 12:35:22 -07001715 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001716 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001717 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001718 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001719 print >> sys.stderr, str(e)
1720 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001721
1722
maruel0eb1d1b2015-10-02 14:48:21 -07001723@subcommand.usage('bot_id')
1724def CMDterminate(parser, args):
1725 """Tells a bot to gracefully shut itself down as soon as it can.
1726
1727 This is done by completing whatever current task there is then exiting the bot
1728 process.
1729 """
1730 parser.add_option(
1731 '--wait', action='store_true', help='Wait for the bot to terminate')
1732 options, args = parser.parse_args(args)
1733 if len(args) != 1:
1734 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001735 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001736 request = net.url_read_json(url, data={})
1737 if not request:
1738 print >> sys.stderr, 'Failed to ask for termination'
1739 return 1
1740 if options.wait:
1741 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001742 options.swarming,
1743 [request['task_id']],
1744 0.,
1745 False,
1746 False,
1747 None,
1748 None,
1749 [],
maruel9531ce02016-04-13 06:11:23 -07001750 False)
maruelbfc5f872017-06-10 16:43:17 -07001751 else:
1752 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001753 return 0
1754
1755
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001756@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001757def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001758 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001759
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001760 Passes all extra arguments provided after '--' as additional command line
1761 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001762 """
1763 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001764 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001765 parser.add_option(
1766 '--dump-json',
1767 metavar='FILE',
1768 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001769 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001770 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001771 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001772 tasks = trigger_task_shards(
1773 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001774 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001775 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001776 tasks_sorted = sorted(
1777 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001778 if options.dump_json:
1779 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001780 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001781 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001782 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001783 }
maruel46b015f2015-10-13 18:40:35 -07001784 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001785 print('To collect results, use:')
1786 print(' swarming.py collect -S %s --json %s' %
1787 (options.swarming, options.dump_json))
1788 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001789 print('To collect results, use:')
1790 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001791 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1792 print('Or visit:')
1793 for t in tasks_sorted:
1794 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001795 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001796 except Failure:
1797 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001798 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001799
1800
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001801class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001802 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001803 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001804 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001805 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001806 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001807 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001808 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001809 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001810 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001811 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001812
1813 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001814 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001815 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001816 auth.process_auth_options(self, options)
1817 user = self._process_swarming(options)
1818 if hasattr(options, 'user') and not options.user:
1819 options.user = user
1820 return options, args
1821
1822 def _process_swarming(self, options):
1823 """Processes the --swarming option and aborts if not specified.
1824
1825 Returns the identity as determined by the server.
1826 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001827 if not options.swarming:
1828 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001829 try:
1830 options.swarming = net.fix_url(options.swarming)
1831 except ValueError as e:
1832 self.error('--swarming %s' % e)
1833 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001834 try:
1835 user = auth.ensure_logged_in(options.swarming)
1836 except ValueError as e:
1837 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001838 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001839
1840
1841def main(args):
1842 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001843 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001844
1845
1846if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001847 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001848 fix_encoding.fix_encoding()
1849 tools.disable_buffering()
1850 colorama.init()
1851 sys.exit(main(sys.argv[1:]))