blob: 4efe64e9e33a6d043e129190c778e6edf27befda [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 [
132 'expiration_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500133 'name',
maruel77f720b2015-09-15 12:35:22 -0700134 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500135 'priority',
maruel77f720b2015-09-15 12:35:22 -0700136 'properties',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700137 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500138 'tags',
139 'user',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500140 ])
141
142
maruel77f720b2015-09-15 12:35:22 -0700143def namedtuple_to_dict(value):
144 """Recursively converts a namedtuple to a dict."""
145 out = dict(value._asdict())
146 for k, v in out.iteritems():
147 if hasattr(v, '_asdict'):
148 out[k] = namedtuple_to_dict(v)
borenet02f772b2016-06-22 12:42:19 -0700149 elif isinstance(v, (list, tuple)):
150 l = []
151 for elem in v:
152 if hasattr(elem, '_asdict'):
153 l.append(namedtuple_to_dict(elem))
154 else:
155 l.append(elem)
156 out[k] = l
maruel77f720b2015-09-15 12:35:22 -0700157 return out
158
159
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700160def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800161 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700162
163 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500164 """
maruel77f720b2015-09-15 12:35:22 -0700165 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700166 # Don't send 'service_account' if it is None to avoid confusing older
167 # version of the server that doesn't know about 'service_account' and don't
168 # use it at all.
169 if not out['service_account']:
170 out.pop('service_account')
maruel77f720b2015-09-15 12:35:22 -0700171 out['properties']['dimensions'] = [
172 {'key': k, 'value': v}
maruelaf6b06c2017-06-08 06:26:53 -0700173 for k, v in out['properties']['dimensions']
maruel77f720b2015-09-15 12:35:22 -0700174 ]
maruel77f720b2015-09-15 12:35:22 -0700175 out['properties']['env'] = [
176 {'key': k, 'value': v}
177 for k, v in out['properties']['env'].iteritems()
178 ]
179 out['properties']['env'].sort(key=lambda x: x['key'])
180 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500181
182
maruel77f720b2015-09-15 12:35:22 -0700183def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500184 """Triggers a request on the Swarming server and returns the json data.
185
186 It's the low-level function.
187
188 Returns:
189 {
190 'request': {
191 'created_ts': u'2010-01-02 03:04:05',
192 'name': ..
193 },
194 'task_id': '12300',
195 }
196 """
197 logging.info('Triggering: %s', raw_request['name'])
198
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500199 result = net.url_read_json(
maruel380e3262016-08-31 16:10:06 -0700200 swarming + '/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500201 if not result:
202 on_error.report('Failed to trigger task %s' % raw_request['name'])
203 return None
maruele557bce2015-11-17 09:01:27 -0800204 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800205 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800206 msg = 'Failed to trigger task %s' % raw_request['name']
207 if result['error'].get('errors'):
208 for err in result['error']['errors']:
209 if err.get('message'):
210 msg += '\nMessage: %s' % err['message']
211 if err.get('debugInfo'):
212 msg += '\nDebug info:\n%s' % err['debugInfo']
213 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800214 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800215
216 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800217 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500218 return result
219
220
221def setup_googletest(env, shards, index):
222 """Sets googletest specific environment variables."""
223 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700224 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
225 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
226 env = env[:]
227 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
228 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500229 return env
230
231
232def trigger_task_shards(swarming, task_request, shards):
233 """Triggers one or many subtasks of a sharded task.
234
235 Returns:
236 Dict with task details, returned to caller as part of --dump-json output.
237 None in case of failure.
238 """
239 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700240 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500241 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700242 req['properties']['env'] = setup_googletest(
243 req['properties']['env'], shards, index)
244 req['name'] += ':%s:%s' % (index, shards)
245 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500246
247 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500248 tasks = {}
249 priority_warning = False
250 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700251 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500252 if not task:
253 break
254 logging.info('Request result: %s', task)
255 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400256 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500257 priority_warning = True
258 print >> sys.stderr, (
259 'Priority was reset to %s' % task['request']['priority'])
260 tasks[request['name']] = {
261 'shard_index': index,
262 'task_id': task['task_id'],
263 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
264 }
265
266 # Some shards weren't triggered. Abort everything.
267 if len(tasks) != len(requests):
268 if tasks:
269 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
270 len(tasks), len(requests))
271 for task_dict in tasks.itervalues():
272 abort_task(swarming, task_dict['task_id'])
273 return None
274
275 return tasks
276
277
278### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000279
280
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700281# How often to print status updates to stdout in 'collect'.
282STATUS_UPDATE_INTERVAL = 15 * 60.
283
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400284
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400285class State(object):
286 """States in which a task can be.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000287
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400288 WARNING: Copy-pasted from appengine/swarming/server/task_result.py. These
289 values are part of the API so if they change, the API changed.
290
291 It's in fact an enum. Values should be in decreasing order of importance.
292 """
293 RUNNING = 0x10
294 PENDING = 0x20
295 EXPIRED = 0x30
296 TIMED_OUT = 0x40
297 BOT_DIED = 0x50
298 CANCELED = 0x60
299 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400300 KILLED = 0x80
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400301
maruel77f720b2015-09-15 12:35:22 -0700302 STATES = (
303 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400304 'COMPLETED', 'KILLED')
maruel77f720b2015-09-15 12:35:22 -0700305 STATES_RUNNING = ('RUNNING', 'PENDING')
306 STATES_NOT_RUNNING = (
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400307 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED', 'KILLED')
308 STATES_DONE = ('TIMED_OUT', 'COMPLETED', 'KILLED')
maruel77f720b2015-09-15 12:35:22 -0700309 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400310
311 _NAMES = {
312 RUNNING: 'Running',
313 PENDING: 'Pending',
314 EXPIRED: 'Expired',
315 TIMED_OUT: 'Execution timed out',
316 BOT_DIED: 'Bot died',
317 CANCELED: 'User canceled',
318 COMPLETED: 'Completed',
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400319 KILLED: 'User killed',
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400320 }
321
maruel77f720b2015-09-15 12:35:22 -0700322 _ENUMS = {
323 'RUNNING': RUNNING,
324 'PENDING': PENDING,
325 'EXPIRED': EXPIRED,
326 'TIMED_OUT': TIMED_OUT,
327 'BOT_DIED': BOT_DIED,
328 'CANCELED': CANCELED,
329 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400330 'KILLED': KILLED,
maruel77f720b2015-09-15 12:35:22 -0700331 }
332
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400333 @classmethod
334 def to_string(cls, state):
335 """Returns a user-readable string representing a State."""
336 if state not in cls._NAMES:
337 raise ValueError('Invalid state %s' % state)
338 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000339
maruel77f720b2015-09-15 12:35:22 -0700340 @classmethod
341 def from_enum(cls, state):
342 """Returns int value based on the string."""
343 if state not in cls._ENUMS:
344 raise ValueError('Invalid state %s' % state)
345 return cls._ENUMS[state]
346
maruel@chromium.org0437a732013-08-27 16:05:52 +0000347
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700348class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700349 """Assembles task execution summary (for --task-summary-json output).
350
351 Optionally fetches task outputs from isolate server to local disk (used when
352 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353
354 This object is shared among multiple threads running 'retrieve_results'
355 function, in particular they call 'process_shard_result' method in parallel.
356 """
357
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000358 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700359 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
360
361 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700362 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700363 shard_count: expected number of task shards.
364 """
maruel12e30012015-10-09 11:55:35 -0700365 self.task_output_dir = (
366 unicode(os.path.abspath(task_output_dir))
367 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000368 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700369 self.shard_count = shard_count
370
371 self._lock = threading.Lock()
372 self._per_shard_results = {}
373 self._storage = None
374
nodire5028a92016-04-29 14:38:21 -0700375 if self.task_output_dir:
376 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700377
Vadim Shtayurab450c602014-05-12 19:23:25 -0700378 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700379 """Stores results of a single task shard, fetches output files if necessary.
380
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400381 Modifies |result| in place.
382
maruel77f720b2015-09-15 12:35:22 -0700383 shard_index is 0-based.
384
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700385 Called concurrently from multiple threads.
386 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700388 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700389 if shard_index < 0 or shard_index >= self.shard_count:
390 logging.warning(
391 'Shard index %d is outside of expected range: [0; %d]',
392 shard_index, self.shard_count - 1)
393 return
394
maruel77f720b2015-09-15 12:35:22 -0700395 if result.get('outputs_ref'):
396 ref = result['outputs_ref']
397 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
398 ref['isolatedserver'],
399 urllib.urlencode(
400 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400401
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700402 # Store result dict of that shard, ignore results we've already seen.
403 with self._lock:
404 if shard_index in self._per_shard_results:
405 logging.warning('Ignoring duplicate shard index %d', shard_index)
406 return
407 self._per_shard_results[shard_index] = result
408
409 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700410 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400411 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700412 result['outputs_ref']['isolatedserver'],
413 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400414 if storage:
415 # Output files are supposed to be small and they are not reused across
416 # tasks. So use MemoryCache for them instead of on-disk cache. Make
417 # files writable, so that calling script can delete them.
418 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700419 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400420 storage,
421 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700422 os.path.join(self.task_output_dir, str(shard_index)),
423 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424
425 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700426 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427 with self._lock:
428 # Write an array of shard results with None for missing shards.
429 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700430 'shards': [
431 self._per_shard_results.get(i) for i in xrange(self.shard_count)
432 ],
433 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000434
435 # Don't store stdout in the summary if not requested too.
436 if "json" not in self.task_output_stdout:
437 for shard_json in summary['shards']:
438 if not shard_json:
439 continue
440 if "output" in shard_json:
441 del shard_json["output"]
442 if "outputs" in shard_json:
443 del shard_json["outputs"]
444
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700445 # Write summary.json to task_output_dir as well.
446 if self.task_output_dir:
447 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700448 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700449 summary,
450 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700451 if self._storage:
452 self._storage.close()
453 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700454 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700455
456 def _get_storage(self, isolate_server, namespace):
457 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700458 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700459 with self._lock:
460 if not self._storage:
461 self._storage = isolateserver.get_storage(isolate_server, namespace)
462 else:
463 # Shards must all use exact same isolate server and namespace.
464 if self._storage.location != isolate_server:
465 logging.error(
466 'Task shards are using multiple isolate servers: %s and %s',
467 self._storage.location, isolate_server)
468 return None
469 if self._storage.namespace != namespace:
470 logging.error(
471 'Task shards are using multiple namespaces: %s and %s',
472 self._storage.namespace, namespace)
473 return None
474 return self._storage
475
476
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500477def now():
478 """Exists so it can be mocked easily."""
479 return time.time()
480
481
maruel77f720b2015-09-15 12:35:22 -0700482def parse_time(value):
483 """Converts serialized time from the API to datetime.datetime."""
484 # When microseconds are 0, the '.123456' suffix is elided. This means the
485 # serialized format is not consistent, which confuses the hell out of python.
486 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
487 try:
488 return datetime.datetime.strptime(value, fmt)
489 except ValueError:
490 pass
491 raise ValueError('Failed to parse %s' % value)
492
493
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700494def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700495 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000496 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400497 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700498
Vadim Shtayurab450c602014-05-12 19:23:25 -0700499 Returns:
500 <result dict> on success.
501 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700502 """
maruel71c61c82016-02-22 06:52:05 -0800503 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700504 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700505 if include_perf:
506 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700507 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700508 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400509 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700510 attempt = 0
511
512 while not should_stop.is_set():
513 attempt += 1
514
515 # Waiting for too long -> give up.
516 current_time = now()
517 if deadline and current_time >= deadline:
518 logging.error('retrieve_results(%s) timed out on attempt %d',
519 base_url, attempt)
520 return None
521
522 # Do not spin too fast. Spin faster at the beginning though.
523 # Start with 1 sec delay and for each 30 sec of waiting add another second
524 # of delay, until hitting 15 sec ceiling.
525 if attempt > 1:
526 max_delay = min(15, 1 + (current_time - started) / 30.0)
527 delay = min(max_delay, deadline - current_time) if deadline else max_delay
528 if delay > 0:
529 logging.debug('Waiting %.1f sec before retrying', delay)
530 should_stop.wait(delay)
531 if should_stop.is_set():
532 return None
533
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400534 # Disable internal retries in net.url_read_json, since we are doing retries
535 # ourselves.
536 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700537 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
538 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400539 # Retry on 500s only if no timeout is specified.
540 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400541 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400542 if timeout == -1:
543 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400544 continue
maruel77f720b2015-09-15 12:35:22 -0700545
maruelbf53e042015-12-01 15:00:51 -0800546 if result.get('error'):
547 # An error occurred.
548 if result['error'].get('errors'):
549 for err in result['error']['errors']:
550 logging.warning(
551 'Error while reading task: %s; %s',
552 err.get('message'), err.get('debugInfo'))
553 elif result['error'].get('message'):
554 logging.warning(
555 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400556 if timeout == -1:
557 return result
maruelbf53e042015-12-01 15:00:51 -0800558 continue
559
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400560 # When timeout == -1, always return on first attempt. 500s are already
561 # retried in this case.
562 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000563 if fetch_stdout:
564 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700565 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700566 # Record the result, try to fetch attached output files (if any).
567 if output_collector:
568 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700569 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700570 if result.get('internal_failure'):
571 logging.error('Internal error!')
572 elif result['state'] == 'BOT_DIED':
573 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700574 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000575
576
maruel77f720b2015-09-15 12:35:22 -0700577def convert_to_old_format(result):
578 """Converts the task result data from Endpoints API format to old API format
579 for compatibility.
580
581 This goes into the file generated as --task-summary-json.
582 """
583 # Sets default.
584 result.setdefault('abandoned_ts', None)
585 result.setdefault('bot_id', None)
586 result.setdefault('bot_version', None)
587 result.setdefault('children_task_ids', [])
588 result.setdefault('completed_ts', None)
589 result.setdefault('cost_saved_usd', None)
590 result.setdefault('costs_usd', None)
591 result.setdefault('deduped_from', None)
592 result.setdefault('name', None)
593 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700594 result.setdefault('server_versions', None)
595 result.setdefault('started_ts', None)
596 result.setdefault('tags', None)
597 result.setdefault('user', None)
598
599 # Convertion back to old API.
600 duration = result.pop('duration', None)
601 result['durations'] = [duration] if duration else []
602 exit_code = result.pop('exit_code', None)
603 result['exit_codes'] = [int(exit_code)] if exit_code else []
604 result['id'] = result.pop('task_id')
605 result['isolated_out'] = result.get('outputs_ref', None)
606 output = result.pop('output', None)
607 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700608 # server_version
609 # Endpoints result 'state' as string. For compatibility with old code, convert
610 # to int.
611 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700612 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700613 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700614 if 'bot_dimensions' in result:
615 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700616 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700617 }
618 else:
619 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700620
621
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700622def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400623 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000624 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500625 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000626
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700627 Duplicate shards are ignored. Shards are yielded in order of completion.
628 Timed out shards are NOT yielded at all. Caller can compare number of yielded
629 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000630
631 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500632 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 +0000633 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500634
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700635 output_collector is an optional instance of TaskOutputCollector that will be
636 used to fetch files produced by a task from isolate server to the local disk.
637
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500638 Yields:
639 (index, result). In particular, 'result' is defined as the
640 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000641 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000642 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400643 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700644 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700645 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700646
maruel@chromium.org0437a732013-08-27 16:05:52 +0000647 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
648 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700649 # Adds a task to the thread pool to call 'retrieve_results' and return
650 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400651 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700652 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000653 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400654 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000655 task_id, timeout, should_stop, output_collector, include_perf,
656 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700657
658 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400659 for shard_index, task_id in enumerate(task_ids):
660 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700661
662 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400663 shards_remaining = range(len(task_ids))
664 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700665 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700666 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700667 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700668 shard_index, result = results_channel.pull(
669 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700670 except threading_utils.TaskChannel.Timeout:
671 if print_status_updates:
672 print(
673 'Waiting for results from the following shards: %s' %
674 ', '.join(map(str, shards_remaining)))
675 sys.stdout.flush()
676 continue
677 except Exception:
678 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700679
680 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700681 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000682 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500683 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000684 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700685
Vadim Shtayurab450c602014-05-12 19:23:25 -0700686 # Yield back results to the caller.
687 assert shard_index in shards_remaining
688 shards_remaining.remove(shard_index)
689 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700690
maruel@chromium.org0437a732013-08-27 16:05:52 +0000691 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700692 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000693 should_stop.set()
694
695
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000696def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000697 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700698 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400699 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700700 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
701 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400702 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
703 metadata.get('abandoned_ts')):
704 pending = '%.1fs' % (
705 parse_time(metadata['abandoned_ts']) -
706 parse_time(metadata['created_ts'])
707 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400708 else:
709 pending = 'N/A'
710
maruel77f720b2015-09-15 12:35:22 -0700711 if metadata.get('duration') is not None:
712 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400713 else:
714 duration = 'N/A'
715
maruel77f720b2015-09-15 12:35:22 -0700716 if metadata.get('exit_code') is not None:
717 # Integers are encoded as string to not loose precision.
718 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400719 else:
720 exit_code = 'N/A'
721
722 bot_id = metadata.get('bot_id') or 'N/A'
723
maruel77f720b2015-09-15 12:35:22 -0700724 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400725 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000726 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400727 if metadata.get('state') == 'CANCELED':
728 tag_footer2 = ' Pending: %s CANCELED' % pending
729 elif metadata.get('state') == 'EXPIRED':
730 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400731 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400732 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
733 pending, duration, bot_id, exit_code, metadata['state'])
734 else:
735 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
736 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400737
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000738 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
739 dash_pad = '+-%s-+' % ('-' * tag_len)
740 tag_header = '| %s |' % tag_header.ljust(tag_len)
741 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
742 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400743
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000744 if include_stdout:
745 return '\n'.join([
746 dash_pad,
747 tag_header,
748 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400749 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000750 dash_pad,
751 tag_footer1,
752 tag_footer2,
753 dash_pad,
754 ])
755 else:
756 return '\n'.join([
757 dash_pad,
758 tag_header,
759 tag_footer2,
760 dash_pad,
761 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000762
763
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700764def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700765 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000766 task_summary_json, task_output_dir, task_output_stdout,
767 include_perf):
maruela5490782015-09-30 10:56:59 -0700768 """Retrieves results of a Swarming task.
769
770 Returns:
771 process exit code that should be returned to the user.
772 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700773 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000774 output_collector = TaskOutputCollector(
775 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700776
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700777 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700778 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400779 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700780 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400781 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400782 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000783 output_collector, include_perf,
784 (len(task_output_stdout) > 0),
785 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700786 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700787
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400788 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700789 shard_exit_code = metadata.get('exit_code')
790 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700791 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700792 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700793 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400794 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700795 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700796
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700797 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000798 s = decorate_shard_output(
799 swarming, index, metadata,
800 "console" in task_output_stdout).encode(
801 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700802 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400803 if len(seen_shards) < len(task_ids):
804 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700805 else:
maruel77f720b2015-09-15 12:35:22 -0700806 print('%s: %s %s' % (
807 metadata.get('bot_id', 'N/A'),
808 metadata['task_id'],
809 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000810 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700811 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400812 if output:
813 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700814 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700815 summary = output_collector.finalize()
816 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700817 # TODO(maruel): Make this optional.
818 for i in summary['shards']:
819 if i:
820 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700821 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700822
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400823 if decorate and total_duration:
824 print('Total duration: %.1fs' % total_duration)
825
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400826 if len(seen_shards) != len(task_ids):
827 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700828 print >> sys.stderr, ('Results from some shards are missing: %s' %
829 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700830 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700831
maruela5490782015-09-30 10:56:59 -0700832 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000833
834
maruel77f720b2015-09-15 12:35:22 -0700835### API management.
836
837
838class APIError(Exception):
839 pass
840
841
842def endpoints_api_discovery_apis(host):
843 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
844 the APIs exposed by a host.
845
846 https://developers.google.com/discovery/v1/reference/apis/list
847 """
maruel380e3262016-08-31 16:10:06 -0700848 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
849 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700850 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
851 if data is None:
852 raise APIError('Failed to discover APIs on %s' % host)
853 out = {}
854 for api in data['items']:
855 if api['id'] == 'discovery:v1':
856 continue
857 # URL is of the following form:
858 # url = host + (
859 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
860 api_data = net.url_read_json(api['discoveryRestUrl'])
861 if api_data is None:
862 raise APIError('Failed to discover %s on %s' % (api['id'], host))
863 out[api['id']] = api_data
864 return out
865
866
maruelaf6b06c2017-06-08 06:26:53 -0700867def get_yielder(base_url, limit):
868 """Returns the first query and a function that yields following items."""
869 CHUNK_SIZE = 250
870
871 url = base_url
872 if limit:
873 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
874 data = net.url_read_json(url)
875 if data is None:
876 # TODO(maruel): Do basic diagnostic.
877 raise Failure('Failed to access %s' % url)
878 org_cursor = data.pop('cursor', None)
879 org_total = len(data.get('items') or [])
880 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
881 if not org_cursor or not org_total:
882 # This is not an iterable resource.
883 return data, lambda: []
884
885 def yielder():
886 cursor = org_cursor
887 total = org_total
888 # Some items support cursors. Try to get automatically if cursors are needed
889 # by looking at the 'cursor' items.
890 while cursor and (not limit or total < limit):
891 merge_char = '&' if '?' in base_url else '?'
892 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
893 if limit:
894 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
895 new = net.url_read_json(url)
896 if new is None:
897 raise Failure('Failed to access %s' % url)
898 cursor = new.get('cursor')
899 new_items = new.get('items')
900 nb_items = len(new_items or [])
901 total += nb_items
902 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
903 yield new_items
904
905 return data, yielder
906
907
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500908### Commands.
909
910
911def abort_task(_swarming, _manifest):
912 """Given a task manifest that was triggered, aborts its execution."""
913 # TODO(vadimsh): No supported by the server yet.
914
915
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400916def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800917 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500918 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500919 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500920 dest='dimensions', metavar='FOO bar',
921 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500922 parser.add_option_group(parser.filter_group)
923
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400924
maruelaf6b06c2017-06-08 06:26:53 -0700925def process_filter_options(parser, options):
926 for key, value in options.dimensions:
927 if ':' in key:
928 parser.error('--dimension key cannot contain ":"')
929 if key.strip() != key:
930 parser.error('--dimension key has whitespace')
931 if not key:
932 parser.error('--dimension key is empty')
933
934 if value.strip() != value:
935 parser.error('--dimension value has whitespace')
936 if not value:
937 parser.error('--dimension value is empty')
938 options.dimensions.sort()
939
940
Vadim Shtayurab450c602014-05-12 19:23:25 -0700941def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400942 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700943 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700944 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700945 help='Number of shards to trigger and collect.')
946 parser.add_option_group(parser.sharding_group)
947
948
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400949def add_trigger_options(parser):
950 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500951 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400952 add_filter_options(parser)
953
maruel681d6802017-01-17 16:56:03 -0800954 group = optparse.OptionGroup(parser, 'Task properties')
955 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700956 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500957 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800958 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500959 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700960 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800961 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800962 '--env-prefix', default=[], action='append', nargs=2,
963 metavar='VAR local/path',
964 help='Prepend task-relative `local/path` to the task\'s VAR environment '
965 'variable using os-appropriate pathsep character. Can be specified '
966 'multiple times for the same VAR to add multiple paths.')
967 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400968 '--idempotent', action='store_true', default=False,
969 help='When set, the server will actively try to find a previous task '
970 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800971 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700972 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700973 help='The optional path to a file containing the secret_bytes to use with'
974 'this task.')
maruel681d6802017-01-17 16:56:03 -0800975 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700976 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400977 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800978 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700979 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400980 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800981 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500982 '--raw-cmd', action='store_true', default=False,
983 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700984 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800985 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500986 '--relative-cwd',
987 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
988 'requires --raw-cmd')
989 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700990 '--cipd-package', action='append', default=[], metavar='PKG',
991 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700992 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800993 group.add_option(
994 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700995 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800996 help='"<name> <relpath>" items to keep a persistent bot managed cache')
997 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700998 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700999 help='Email of a service account to run the task as, or literal "bot" '
1000 'string to indicate that the task should use the same account the '
1001 'bot itself is using to authenticate to Swarming. Don\'t use task '
1002 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001003 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001004 '-o', '--output', action='append', default=[], metavar='PATH',
1005 help='A list of files to return in addition to those written to '
1006 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1007 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -08001008 parser.add_option_group(group)
1009
1010 group = optparse.OptionGroup(parser, 'Task request')
1011 group.add_option(
1012 '--priority', type='int', default=100,
1013 help='The lower value, the more important the task is')
1014 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001015 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001016 help='Display name of the task. Defaults to '
1017 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1018 'isolated file is provided, if a hash is provided, it defaults to '
1019 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1020 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001021 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001022 help='Tags to assign to the task.')
1023 group.add_option(
1024 '--user', default='',
1025 help='User associated with the task. Defaults to authenticated user on '
1026 'the server.')
1027 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001028 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001029 help='Seconds to allow the task to be pending for a bot to run before '
1030 'this task request expires.')
1031 group.add_option(
1032 '--deadline', type='int', dest='expiration',
1033 help=optparse.SUPPRESS_HELP)
1034 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001035
1036
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001037def process_trigger_options(parser, options, args):
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001038 """Processes trigger options and does preparatory steps."""
maruelaf6b06c2017-06-08 06:26:53 -07001039 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001040 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001041 if args and args[0] == '--':
1042 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001043
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001044 if not options.dimensions:
1045 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001046 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1047 parser.error('--tags must be in the format key:value')
1048 if options.raw_cmd and not args:
1049 parser.error(
1050 'Arguments with --raw-cmd should be passed after -- as command '
1051 'delimiter.')
1052 if options.isolate_server and not options.namespace:
1053 parser.error(
1054 '--namespace must be a valid value when --isolate-server is used')
1055 if not options.isolated and not options.raw_cmd:
1056 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1057
1058 # Isolated
1059 # --isolated is required only if --raw-cmd wasn't provided.
1060 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1061 # preferred server.
1062 isolateserver.process_isolate_server_options(
1063 parser, options, False, not options.raw_cmd)
1064 inputs_ref = None
1065 if options.isolate_server:
1066 inputs_ref = FilesRef(
1067 isolated=options.isolated,
1068 isolatedserver=options.isolate_server,
1069 namespace=options.namespace)
1070
1071 # Command
1072 command = None
1073 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001074 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001075 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001076 if options.relative_cwd:
1077 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1078 if not a.startswith(os.getcwd()):
1079 parser.error(
1080 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001081 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001082 if options.relative_cwd:
1083 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001084 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001085
maruel0a25f6c2017-05-10 10:43:23 -07001086 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001087 cipd_packages = []
1088 for p in options.cipd_package:
1089 split = p.split(':', 2)
1090 if len(split) != 3:
1091 parser.error('CIPD packages must take the form: path:package:version')
1092 cipd_packages.append(CipdPackage(
1093 package_name=split[1],
1094 path=split[0],
1095 version=split[2]))
1096 cipd_input = None
1097 if cipd_packages:
1098 cipd_input = CipdInput(
1099 client_package=None,
1100 packages=cipd_packages,
1101 server=None)
1102
maruel0a25f6c2017-05-10 10:43:23 -07001103 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001104 secret_bytes = None
1105 if options.secret_bytes_path:
1106 with open(options.secret_bytes_path, 'r') as f:
1107 secret_bytes = f.read().encode('base64')
1108
maruel0a25f6c2017-05-10 10:43:23 -07001109 # Named caches
maruel681d6802017-01-17 16:56:03 -08001110 caches = [
1111 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1112 for i in options.named_cache
1113 ]
maruel0a25f6c2017-05-10 10:43:23 -07001114
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001115 env_prefixes = {}
1116 for k, v in options.env_prefix:
1117 env_prefixes.setdefault(k, []).append(v)
1118
maruel77f720b2015-09-15 12:35:22 -07001119 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001120 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001121 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001122 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001123 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001124 dimensions=options.dimensions,
1125 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001126 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001127 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001128 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001129 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001130 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001131 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001132 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001133 outputs=options.output,
1134 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001135
maruel77f720b2015-09-15 12:35:22 -07001136 return NewTaskRequest(
1137 expiration_secs=options.expiration,
maruel0a25f6c2017-05-10 10:43:23 -07001138 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001139 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001140 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001141 properties=properties,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001142 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001143 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001144 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001145
1146
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001147class TaskOutputStdoutOption(optparse.Option):
1148 """Where to output the each task's console output (stderr/stdout).
1149
1150 The output will be;
1151 none - not be downloaded.
1152 json - stored in summary.json file *only*.
1153 console - shown on stdout *only*.
1154 all - stored in summary.json and shown on stdout.
1155 """
1156
1157 choices = ['all', 'json', 'console', 'none']
1158
1159 def __init__(self, *args, **kw):
1160 optparse.Option.__init__(
1161 self,
1162 *args,
1163 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001164 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001165 help=re.sub('\s\s*', ' ', self.__doc__),
1166 **kw)
1167
1168 def convert_value(self, opt, value):
1169 if value not in self.choices:
1170 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1171 self.get_opt_string(), self.choices, value))
1172 stdout_to = []
1173 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001174 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001175 elif value != 'none':
1176 stdout_to = [value]
1177 return stdout_to
1178
1179
maruel@chromium.org0437a732013-08-27 16:05:52 +00001180def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001181 parser.server_group.add_option(
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001182 '-t', '--timeout', type='float', default=0,
1183 help='Timeout to wait for result, set to -1 for no timeout and get '
1184 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001185 parser.group_logging.add_option(
1186 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001187 parser.group_logging.add_option(
1188 '--print-status-updates', action='store_true',
1189 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001190 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001191 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001192 '--task-summary-json',
1193 metavar='FILE',
1194 help='Dump a summary of task results to this file as json. It contains '
1195 'only shards statuses as know to server directly. Any output files '
1196 'emitted by the task can be collected by using --task-output-dir')
1197 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001198 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001199 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001200 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001201 'directory contains per-shard directory with output files produced '
1202 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001203 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001204 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001205 parser.task_output_group.add_option(
1206 '--perf', action='store_true', default=False,
1207 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001208 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001209
1210
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001211def process_collect_options(parser, options):
1212 # Only negative -1 is allowed, disallow other negative values.
1213 if options.timeout != -1 and options.timeout < 0:
1214 parser.error('Invalid --timeout value')
1215
1216
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001217@subcommand.usage('bots...')
1218def CMDbot_delete(parser, args):
1219 """Forcibly deletes bots from the Swarming server."""
1220 parser.add_option(
1221 '-f', '--force', action='store_true',
1222 help='Do not prompt for confirmation')
1223 options, args = parser.parse_args(args)
1224 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001225 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001226
1227 bots = sorted(args)
1228 if not options.force:
1229 print('Delete the following bots?')
1230 for bot in bots:
1231 print(' %s' % bot)
1232 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1233 print('Goodbye.')
1234 return 1
1235
1236 result = 0
1237 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001238 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001239 if net.url_read_json(url, data={}, method='POST') is None:
1240 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001241 result = 1
1242 return result
1243
1244
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001245def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001246 """Returns information about the bots connected to the Swarming server."""
1247 add_filter_options(parser)
1248 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001249 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001250 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001251 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001252 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001253 help='Keep both dead and alive bots')
1254 parser.filter_group.add_option(
1255 '--busy', action='store_true', help='Keep only busy bots')
1256 parser.filter_group.add_option(
1257 '--idle', action='store_true', help='Keep only idle bots')
1258 parser.filter_group.add_option(
1259 '--mp', action='store_true',
1260 help='Keep only Machine Provider managed bots')
1261 parser.filter_group.add_option(
1262 '--non-mp', action='store_true',
1263 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001264 parser.filter_group.add_option(
1265 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001266 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001267 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001268 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001269
1270 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001271 parser.error('Use only one of --keep-dead or --dead-only')
1272 if options.busy and options.idle:
1273 parser.error('Use only one of --busy or --idle')
1274 if options.mp and options.non_mp:
1275 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001276
maruelaf6b06c2017-06-08 06:26:53 -07001277 url = options.swarming + '/api/swarming/v1/bots/list?'
1278 values = []
1279 if options.dead_only:
1280 values.append(('is_dead', 'TRUE'))
1281 elif options.keep_dead:
1282 values.append(('is_dead', 'NONE'))
1283 else:
1284 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001285
maruelaf6b06c2017-06-08 06:26:53 -07001286 if options.busy:
1287 values.append(('is_busy', 'TRUE'))
1288 elif options.idle:
1289 values.append(('is_busy', 'FALSE'))
1290 else:
1291 values.append(('is_busy', 'NONE'))
1292
1293 if options.mp:
1294 values.append(('is_mp', 'TRUE'))
1295 elif options.non_mp:
1296 values.append(('is_mp', 'FALSE'))
1297 else:
1298 values.append(('is_mp', 'NONE'))
1299
1300 for key, value in options.dimensions:
1301 values.append(('dimensions', '%s:%s' % (key, value)))
1302 url += urllib.urlencode(values)
1303 try:
1304 data, yielder = get_yielder(url, 0)
1305 bots = data.get('items') or []
1306 for items in yielder():
1307 if items:
1308 bots.extend(items)
1309 except Failure as e:
1310 sys.stderr.write('\n%s\n' % e)
1311 return 1
maruel77f720b2015-09-15 12:35:22 -07001312 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001313 print bot['bot_id']
1314 if not options.bare:
1315 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1316 print ' %s' % json.dumps(dimensions, sort_keys=True)
1317 if bot.get('task_id'):
1318 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001319 return 0
1320
1321
maruelfd0a90c2016-06-10 11:51:10 -07001322@subcommand.usage('task_id')
1323def CMDcancel(parser, args):
1324 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001325 parser.add_option(
1326 '-k', '--kill-running', action='store_true', default=False,
1327 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001328 options, args = parser.parse_args(args)
1329 if not args:
1330 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001331 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001332 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001333 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001334 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001335 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001336 print('Deleting %s failed. Probably already gone' % task_id)
1337 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001338 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001339 return 0
1340
1341
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001342@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001343def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001344 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001345
1346 The result can be in multiple part if the execution was sharded. It can
1347 potentially have retries.
1348 """
1349 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001350 parser.add_option(
1351 '-j', '--json',
1352 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001353 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001354 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001355 if not args and not options.json:
1356 parser.error('Must specify at least one task id or --json.')
1357 if args and options.json:
1358 parser.error('Only use one of task id or --json.')
1359
1360 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001361 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001362 try:
maruel1ceb3872015-10-14 06:10:44 -07001363 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001364 data = json.load(f)
1365 except (IOError, ValueError):
1366 parser.error('Failed to open %s' % options.json)
1367 try:
1368 tasks = sorted(
1369 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1370 args = [t['task_id'] for t in tasks]
1371 except (KeyError, TypeError):
1372 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001373 if not options.timeout:
maruel71c61c82016-02-22 06:52:05 -08001374 options.timeout = (
1375 data['request']['properties']['execution_timeout_secs'] +
1376 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001377 else:
1378 valid = frozenset('0123456789abcdef')
1379 if any(not valid.issuperset(task_id) for task_id in args):
1380 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001381
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001382 try:
1383 return collect(
1384 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001385 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001386 options.timeout,
1387 options.decorate,
1388 options.print_status_updates,
1389 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001390 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001391 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001392 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001393 except Failure:
1394 on_error.report(None)
1395 return 1
1396
1397
maruel77f720b2015-09-15 12:35:22 -07001398@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001399def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001400 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1401 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001402
1403 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001404 Raw task request and results:
1405 swarming.py query -S server-url.com task/123456/request
1406 swarming.py query -S server-url.com task/123456/result
1407
maruel77f720b2015-09-15 12:35:22 -07001408 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001409 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001410
maruelaf6b06c2017-06-08 06:26:53 -07001411 Listing last 10 tasks on a specific bot named 'bot1':
1412 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001413
maruelaf6b06c2017-06-08 06:26:53 -07001414 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001415 quoting is important!:
1416 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001417 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001418 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001419 parser.add_option(
1420 '-L', '--limit', type='int', default=200,
1421 help='Limit to enforce on limitless items (like number of tasks); '
1422 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001423 parser.add_option(
1424 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001425 parser.add_option(
1426 '--progress', action='store_true',
1427 help='Prints a dot at each request to show progress')
1428 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001429 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001430 parser.error(
1431 'Must specify only method name and optionally query args properly '
1432 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001433 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001434 try:
1435 data, yielder = get_yielder(base_url, options.limit)
1436 for items in yielder():
1437 if items:
1438 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001439 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001440 sys.stderr.write('.')
1441 sys.stderr.flush()
1442 except Failure as e:
1443 sys.stderr.write('\n%s\n' % e)
1444 return 1
maruel77f720b2015-09-15 12:35:22 -07001445 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001446 sys.stderr.write('\n')
1447 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001448 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001449 options.json = unicode(os.path.abspath(options.json))
1450 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001451 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001452 try:
maruel77f720b2015-09-15 12:35:22 -07001453 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001454 sys.stdout.write('\n')
1455 except IOError:
1456 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001457 return 0
1458
1459
maruel77f720b2015-09-15 12:35:22 -07001460def CMDquery_list(parser, args):
1461 """Returns list of all the Swarming APIs that can be used with command
1462 'query'.
1463 """
1464 parser.add_option(
1465 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1466 options, args = parser.parse_args(args)
1467 if args:
1468 parser.error('No argument allowed.')
1469
1470 try:
1471 apis = endpoints_api_discovery_apis(options.swarming)
1472 except APIError as e:
1473 parser.error(str(e))
1474 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001475 options.json = unicode(os.path.abspath(options.json))
1476 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001477 json.dump(apis, f)
1478 else:
1479 help_url = (
1480 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1481 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001482 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1483 if i:
1484 print('')
maruel77f720b2015-09-15 12:35:22 -07001485 print api_id
maruel11e31af2017-02-15 07:30:50 -08001486 print ' ' + api['description'].strip()
1487 if 'resources' in api:
1488 # Old.
1489 for j, (resource_name, resource) in enumerate(
1490 sorted(api['resources'].iteritems())):
1491 if j:
1492 print('')
1493 for method_name, method in sorted(resource['methods'].iteritems()):
1494 # Only list the GET ones.
1495 if method['httpMethod'] != 'GET':
1496 continue
1497 print '- %s.%s: %s' % (
1498 resource_name, method_name, method['path'])
1499 print('\n'.join(
1500 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1501 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1502 else:
1503 # New.
1504 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001505 # Only list the GET ones.
1506 if method['httpMethod'] != 'GET':
1507 continue
maruel11e31af2017-02-15 07:30:50 -08001508 print '- %s: %s' % (method['id'], method['path'])
1509 print('\n'.join(
1510 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001511 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1512 return 0
1513
1514
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001515@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001516def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001517 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001518
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001519 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001520 """
1521 add_trigger_options(parser)
1522 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001523 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001524 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001525 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001526 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001527 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001528 tasks = trigger_task_shards(
1529 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001530 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001531 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001532 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001533 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001534 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001535 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001536 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001537 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001538 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001539 task_ids = [
1540 t['task_id']
1541 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1542 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001543 if not options.timeout:
maruel71c61c82016-02-22 06:52:05 -08001544 options.timeout = (
1545 task_request.properties.execution_timeout_secs +
1546 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001547 try:
1548 return collect(
1549 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001550 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001551 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001552 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001553 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001554 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001555 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001556 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001557 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001558 except Failure:
1559 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001560 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001561
1562
maruel18122c62015-10-23 06:31:23 -07001563@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001564def CMDreproduce(parser, args):
1565 """Runs a task locally that was triggered on the server.
1566
1567 This running locally the same commands that have been run on the bot. The data
1568 downloaded will be in a subdirectory named 'work' of the current working
1569 directory.
maruel18122c62015-10-23 06:31:23 -07001570
1571 You can pass further additional arguments to the target command by passing
1572 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001573 """
maruelc070e672016-02-22 17:32:57 -08001574 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001575 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001576 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001577 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001578 extra_args = []
1579 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001580 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001581 if len(args) > 1:
1582 if args[1] == '--':
1583 if len(args) > 2:
1584 extra_args = args[2:]
1585 else:
1586 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001587
maruel380e3262016-08-31 16:10:06 -07001588 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001589 request = net.url_read_json(url)
1590 if not request:
1591 print >> sys.stderr, 'Failed to retrieve request data for the task'
1592 return 1
1593
maruel12e30012015-10-09 11:55:35 -07001594 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001595 if fs.isdir(workdir):
1596 parser.error('Please delete the directory \'work\' first')
1597 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001598 cachedir = unicode(os.path.abspath('cipd_cache'))
1599 if not fs.exists(cachedir):
1600 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001601
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001602 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001603 env = os.environ.copy()
1604 env['SWARMING_BOT_ID'] = 'reproduce'
1605 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001606 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001607 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001608 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001609 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001610 if not i['value']:
1611 env.pop(key, None)
1612 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001613 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001614
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001615 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001616 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001617 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001618 for i in env_prefixes:
1619 key = i['key']
1620 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001621 cur = env.get(key)
1622 if cur:
1623 paths.append(cur)
1624 env[key] = os.path.pathsep.join(paths)
1625
iannucci31ab9192017-05-02 19:11:56 -07001626 command = []
nodir152cba62016-05-12 16:08:56 -07001627 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001628 # Create the tree.
1629 with isolateserver.get_storage(
1630 properties['inputs_ref']['isolatedserver'],
1631 properties['inputs_ref']['namespace']) as storage:
1632 bundle = isolateserver.fetch_isolated(
1633 properties['inputs_ref']['isolated'],
1634 storage,
1635 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001636 workdir,
1637 False)
maruel29ab2fd2015-10-16 11:44:01 -07001638 command = bundle.command
1639 if bundle.relative_cwd:
1640 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001641 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001642
1643 if properties.get('command'):
1644 command.extend(properties['command'])
1645
1646 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001647 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001648 if not options.output_dir:
1649 new_command = run_isolated.process_command(command, 'invalid', None)
1650 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001651 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001652 else:
1653 # Make the path absolute, as the process will run from a subdirectory.
1654 options.output_dir = os.path.abspath(options.output_dir)
1655 new_command = run_isolated.process_command(
1656 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001657 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001658 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001659 command = new_command
1660 file_path.ensure_command_has_abs_path(command, workdir)
1661
1662 if properties.get('cipd_input'):
1663 ci = properties['cipd_input']
1664 cp = ci['client_package']
1665 client_manager = cipd.get_client(
1666 ci['server'], cp['package_name'], cp['version'], cachedir)
1667
1668 with client_manager as client:
1669 by_path = collections.defaultdict(list)
1670 for pkg in ci['packages']:
1671 path = pkg['path']
1672 # cipd deals with 'root' as ''
1673 if path == '.':
1674 path = ''
1675 by_path[path].append((pkg['package_name'], pkg['version']))
1676 client.ensure(workdir, by_path, cache_dir=cachedir)
1677
maruel77f720b2015-09-15 12:35:22 -07001678 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001679 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001680 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001681 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001682 print >> sys.stderr, str(e)
1683 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001684
1685
maruel0eb1d1b2015-10-02 14:48:21 -07001686@subcommand.usage('bot_id')
1687def CMDterminate(parser, args):
1688 """Tells a bot to gracefully shut itself down as soon as it can.
1689
1690 This is done by completing whatever current task there is then exiting the bot
1691 process.
1692 """
1693 parser.add_option(
1694 '--wait', action='store_true', help='Wait for the bot to terminate')
1695 options, args = parser.parse_args(args)
1696 if len(args) != 1:
1697 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001698 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001699 request = net.url_read_json(url, data={})
1700 if not request:
1701 print >> sys.stderr, 'Failed to ask for termination'
1702 return 1
1703 if options.wait:
1704 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001705 options.swarming,
1706 [request['task_id']],
1707 0.,
1708 False,
1709 False,
1710 None,
1711 None,
1712 [],
maruel9531ce02016-04-13 06:11:23 -07001713 False)
maruelbfc5f872017-06-10 16:43:17 -07001714 else:
1715 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001716 return 0
1717
1718
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001719@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001720def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001721 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001722
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001723 Passes all extra arguments provided after '--' as additional command line
1724 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001725 """
1726 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001727 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001728 parser.add_option(
1729 '--dump-json',
1730 metavar='FILE',
1731 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001732 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001733 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001734 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001735 tasks = trigger_task_shards(
1736 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001737 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001738 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001739 tasks_sorted = sorted(
1740 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001741 if options.dump_json:
1742 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001743 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001744 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001745 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001746 }
maruel46b015f2015-10-13 18:40:35 -07001747 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001748 print('To collect results, use:')
1749 print(' swarming.py collect -S %s --json %s' %
1750 (options.swarming, options.dump_json))
1751 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001752 print('To collect results, use:')
1753 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001754 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1755 print('Or visit:')
1756 for t in tasks_sorted:
1757 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001758 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001759 except Failure:
1760 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001761 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001762
1763
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001764class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001765 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001766 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001767 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001768 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001769 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001770 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001771 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001772 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001773 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001774 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001775
1776 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001777 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001778 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001779 auth.process_auth_options(self, options)
1780 user = self._process_swarming(options)
1781 if hasattr(options, 'user') and not options.user:
1782 options.user = user
1783 return options, args
1784
1785 def _process_swarming(self, options):
1786 """Processes the --swarming option and aborts if not specified.
1787
1788 Returns the identity as determined by the server.
1789 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001790 if not options.swarming:
1791 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001792 try:
1793 options.swarming = net.fix_url(options.swarming)
1794 except ValueError as e:
1795 self.error('--swarming %s' % e)
1796 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001797 try:
1798 user = auth.ensure_logged_in(options.swarming)
1799 except ValueError as e:
1800 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001801 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001802
1803
1804def main(args):
1805 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001806 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001807
1808
1809if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001810 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001811 fix_encoding.fix_encoding()
1812 tools.disable_buffering()
1813 colorama.init()
1814 sys.exit(main(sys.argv[1:]))